ボクココ

個人開発に関するテックブログ

スマホアプリ開発に先駆けて

ども、@kimihom です。

2021 年の新しい挑戦の1つとして、スマホアプリ開発がある。 私自身の経験を伝えると、6,7年前ごろに Android アプリで Java で開発していたことがある。これは仕事でガッツリ実装してたので、だいぶ詳しくはなかったが、それでもだいぶ前なので技術として参考になることは少ないだろう。そもそもプログラミング言語自体 Kotlin に変わっている。

また、iPhone アプリも開発していたことがある。Swift v1.0 が出立ての頃に、個人の趣味で作った Android アプリの iPhone 版を作ってリリースした。そのアプリは Android マーケットより厳しい iPhone マーケットで受け入れられず、しばらくしたらマーケットから消えてしまっていた。それを改善するほどの個人のやる気もなかったので、そのままスルーで終わった形である。一応 Swift で書いていた程度である。

さて、そのくらいの開発経験で、今どうやってスマホアプリの実装をしていくか考えた。

今、Flutter と呼ばれるプラットフォームが流行っているようだ。開発元は Google。コードを Dartと呼ばれる独自プログラミング言語で書き、それを iPhone, Android アプリで動くように変換されるようだ。UI はプラットフォーム側のではなく、独自で作られたのを共通で使うとのことである。なるほど、Dart が選ばれる理由もよくわかる。確実に iPhone, Android アプリ両方作るなら、この選択肢をまず真っ先に考えるだろう。

f:id:cevid_cpp:20201217084925p:plain

もし私が、単純な API 呼び出しとシンプルな UI 構成でできたアプリを作るのであれば、確実に採用していただろう。ただ、これから作ろうとするものが何かという点でより詳しい検討が必要である。

来年以降開発をしようとしているアプリは、サードパーティが開発運用しているライブラリを利用予定である。そして、そのライブラリが、完全に Swift, Kotlin で書く前提のライブラリであった。もちろん、Dart で頑張って書くという選択肢もあるけど、ドキュメントが全て Swift, Kotlin である以上、明らかにそれぞれのプログラミング言語でドキュメントを読みながら作った方が安全で正しくアプリが作れる。また、私自身 Swift の経験があるし、あとは似た言語? のイメージである Kotlin を学ぶだけなのでそこまで苦戦しなさそうというイメージを持っている。

f:id:cevid_cpp:20201217090049p:plain

最初から複雑な UI や機能を実装する計画でもなく、まずはその サードパーティのライブラリを組み込んでアカウントログインさえできればと考えている。その程度であれば、Swiftと Kotlin それぞれで書くこともそこまで辛いことではなさそうだ。

1人が2つのアプリを作るとなるけども、幸いどちらも経験がある。 Android Studio と Xcode という重たいソフトウェアを2つ入れるのが億劫なくらいである。

開発のきっかけ

今や、スマホアプリは個人で使うだけのものではなく、仕事でも当たり前に使われるようになっている。少し前までは、会社の電話はスマホではなく古い感じの携帯電話を使っている人も多かったけど、最近はもうビジネスで使うサブの電話もスマホになっている。

そうすると、ビジネスで普段使うスマホに、ビジネス用の Android/iPhone アプリのニーズがどんどん高まってくる。以前の 私の Android/iPhone 開発は、完全に個人向けのものだったけど、来年はビジネス向けという全く違うターゲットで開発を初めていく予定だ。

終わりに

来年の大きな技術的挑戦の一つスマホアプリの現在の考えについて記した。

まだ現時点での検討ということもあるけど、既に Swift UI という新しくなった iPhone アプリ開発を調べ始めているところだ。既に来年の挑戦は始まっている。

Web がメインだったボクココの記事ではあるけど、今後はアプリ開発におけるネタも多く出てきそうだ。

年末年始にどれだけ勉強して開発に持っていけるか。その空いた時間に何もしないか学ぶかで大きな差が出てくるので頑張っていこう。

Firebase はアプリ開発者のヒーローとなるか

ども、@kimihom です。

f:id:cevid_cpp:20160713231835p:plain

今日 Firebase の勉強会に行った。あらかじめ動画とかで Firebase の予習はしてたんだけど、今回の勉強会で実際のユースケースとか具体的な使い方を知ったことでよりイメージが湧いた。そこで感じた Firebase について今回はちょろっと語ろうと思う。

アプリ開発におけるサーバー/サービス選択の歴史

基本的にネットワークを使わずにアプリだけで完結するアプリってのは単純なものが多く、ユーザーを獲得することは困難になりつつある。より良いアプリにするには、クラウドと連携して"つながる"とか"最新情報がある"アプリにすることが大事であることは自明なことかと思う。

そんな中でエンジニアがアプリ開発している時に大抵決まって同じような悩みを抱く。その典型が"サーバーどうするの"っていう話。サーバーがなければ画像を置くこともできないし、ユーザーを管理することもできず、情報を取得/更新することもできない。要するに何もできないってわけだ。

今までは自前で API サーバーを持つことが一般的だった。これは今までの Web 知識を用いれば多少時間がかかっても実装が可能だし、Webアプリとの連携もデータベースを共有することで可能になる。 だから大手とか小さいところでも自前で API サーバーを持つことが多かった。というよりそれくらいしか選択肢がなかった。

でもみんなやるよねって機能はやはり楽をしたいもの。 AWS をはじめとしたモバイルのプラットフォームがどんどん出てきて、アプリ開発者が自前でサーバー実装しなければならないことを極端に少なくしてくれた。アプリエンジニアはそれらを活用することでより早くより高品質なアプリを作り出すことができる。

しかし、次に問題になってくるのは色々なアプリ開発のモバイルプラットフォームを使うと、アカウント登録がその都度必要で、クレジットカードとか登録情報とかいちいち管理しなければならない問題が出てくる。またそれらの情報を統合的に管理することができず、それぞれがそれぞれのデータを持って分断された分析や機能の追加しかできなかったわけだ。AWS のモバイル系のサービスは各種機能を統合することを実現してはいるが、"アプリ開発に特化した"って意味では弱い部分があると個人的に思う。AWSをはじめとした総合的な開発プラットフォームでは、各種サービスが Webでもモバイルでも使える汎用的な部分が多く、アプリだけで欲しい特化したサービスが弱いと感じている。

そこでつい最近に進化した Firebase である。進化した Firebase は"モバイル開発において必要だよねー"ってところを基本的にすべて統括的に提供している。Firebase にまとめて情報を管理するようにすれば、断片的だったデータを一元管理できるようになり、使う SDK も最低限のものだけにすることができる。いろんなSDKを詰め込むようなことはしなくて良いし、余計なコードを書く必要もない。(Firebase の各機能は Gradle 経由で簡単にインストールできる)。 確かに機能的には他の専門ツール(例えば分析系)に劣る部分はあるかもしれない。ただそれも時間の問題で、ゆくゆくは Firebase だけですべてがまかなえるような、そんなプラットフォームになっていくだろう。

特に Firebase では Web とアプリの垣根をなくしていくという点で、Deep Link や App Indexing などの Google 検索などに親和性の高い機能も提供しており、これらは Firebase ならではという感じがしている。今までのFirebaseであるリアルタイムデータベースもそうだし、 Remote Config もまさにアプリだからこそ欲しい機能であり、AWS など汎用的なクラウドプラットフォームでは提供できていない部分である。

開発プラットフォームの選択

私はこのような思いからアプリ開発に特化した開発プロジェクトを始めるなら Firebase を使う。というか使い始める予定である。

しかしアプリに特化というよりも、 CGM(ユーザーがログインして投稿しあうようなサービス) のような従来の Web サービスをそのままアプリにするっていうのであれば、 Rails の Turbolinks などを活用したアプリ作成の方がいい選択になる場合もあると思っている。最近のHTML5の技術と、それ+@ 程度の機能の要件のサービスであれば、Webベースのアプリを検討すべきである。この場合は Firebase を使う必要はほとんどないだろう。詳細は以下の記事に書いた。

www.bokukoko.info

当たり前の話ではあるが大事なのは適材適所である。

私の Firebase に対しての唯一の要望

そんな私が絶賛する Firebase だが、あと一つ欲しいものがある。それがサーバーサイドでロジックを実行できる、 Cloud Functions と Firebase の連携だ。やはりどうしてもサーバーサイドで実行したいようなロジックがあるときに、Firebase で Cloud Functions と同等の機能があれば、もう他のアプリ開発サービスを使う必要はなくなってくると思う。わざわざちょっとしたロジックだけのために Google App Engine や Google Cloud Platform などは使いたくないってケースは大いにあるだろう。

この願いを持っている人は他にもいるみたいで、今 Google では検討中? な感じのようである。

Google グループ

終わりに

この記事を書いた背景としては、まず進化した Firebase がすごいと感動したからであり、その凄さをまだほとんどの人は知らないんじゃないかという思いで書いたというのがある。

一部の熱狂的なアプリ開発者だけが使われて、それらのアプリだけが差別化してどんどん成長していくってのはもったいない気がしたので、ぜひ経験の浅いアプリ開発エンジニアでも Firebase にチャレンジしてみて欲しいと思う。

ドキュメントはやはり英語だから辛い部分もあるかと思う。でも英語を読むだけで実装のほとんどをやらなくて済むようになるので、頑張ってみて欲しい。

私もこれからどんどん Firebase を使っていくが、そこで得た知見は適宜このブログで書いていきたいと思う。

スワイプでViewが移動するViewを作った

SwipePalletViewOSSで公開した。

SwipePalletView

こんな感じでちょっとスワイプしたら端っこまで自動で移動するようなViewだ。

使い方とかは上記サイトを見ていただくとして、今回は androidで初めてカスタムビューを作ったので、その作り方的なのを書いてみる。

カスタムビューの基本

まずは、RelativeLayout などのViewを継承したクラスを作る。そのあとに各Viewで必要なコンストラクタを定義。

class CustomView extends RelativeLayout {
    public SwipePalletView(Context context) {
        super(context);
        init(null);
    }

    public SwipePalletView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public SwipePalletView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SwipePalletView);
            float scale = a.getFloat(R.styleable.SwipePalletView_widthScale, 0.5f);
            int duration = a.getInteger(R.styleable.SwipePalletView_horizontalDuration, 500);
            a.recycle();
        }
    }
}

このattrは、レイアウトXMLで定義した属性値が入っている。

Styleable

res/attrs.xmlを作成する。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SwipePalletView">
        <attr name="widthScale" format="float" />
        <attr name="horizontalDuration" format="integer" />
    </declare-styleable>
</resources>

こうすることで、以下のようにXMLに属性を追加できる。

    <com.honkimi.swipepalletview.SwipePalletVerticalView
        custom:heightScale="0.3"
        custom:verticalDuration="700"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </com.honkimi.swipepalletview.SwipePalletVerticalView>

attr の format は色々な形式で指定できる。

Viewの作成

後はそれぞれ独自のViewの挙動を作ってあげればいい。

TODO

build.gradle で compile 'com.github.traex.rippleeffect:library:1.2.3' みたいな指定でビルドできるようにしたいが、何かよくわからなかったのでできなかった。時間あるときにまたチャレンジしてみよう。

ViewPager を DialogFragment で表示する

Android ではおなじみの ViewPager。 これを使えば、スワイプで画面切り替えを簡単に実装することができる。スマホアプリらしいアプリを作るなら、これはよく使うと思う。

さて、これをダイアログで出したいと思った時の手順をここにまとめよう。思いの外はまってしまった。。今回は DialogFragment を初めて使った。

DialogFragment とは

今まで自分は、AlertDialog.Builder でダイアログを表示していた。これとの違いをリファレンスで読んでみると、

A fragment that displays a dialog window, floating on top of its activity's window. This fragment contains a Dialog object, which it displays as appropriate based on the fragment's state. Control of the dialog (deciding when to show, hide, dismiss it) should be done through the API here, not with direct calls on the dialog.

ダイアログウィンドウを表示するフラグメントで、アクティビティウィンドウのトップに浮遊する。このフラグメントはフラグメントの状態に応じて適切に表示されるダイアログオブジェクトを含んでいる。ダイアログのコントロール(いつ表示し、いつ非表示にするか)は、そのAPIを通じて行われるべきであり、ダイアログに対して直接呼び出すべきではない。

要はこのフラグメント内で、AlertDialogを生成して、それをコントロールすべきであり、直接AlertDialog.Builderを使うべきではないということらしい。なので今後はカスタマイズしたダイアログ出すときはDialogFragmentを使っていく感じにしよう。

ViewPager を利用する

さて、ここからが本題。 FragmentDialog を使って ViewPager のスワイプを実装しよう。今回はシンプルに取得した画像 URLをそのままスワイプで表示させるサンプル。

ダイアログ内のスワイプ一つ一つのページ

PageFragment.java

public class PageFragment extends Fragment {
    private final static String URL = "url";

    public static PageFragment newInstance(String url) {
        PageFragment fragment = new PageFragment();
        Bundle args = new Bundle();
        args.putString(URL, url);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Bundle bundle = getArguments();
        String url = bundle.getString(URL);
        NetworkImageView networkImageView = new NetworkImageView(getActivity());
        networkImageView.setImageUrl(url, ApplicationController.getInstance().getImageLoader());
        return networkImageView;
    }
}

Fragment の値の受け渡しには、Bundleを利用する。引数やセッターではないことに注意。

各PageFragmentを管理するAdapter

public class PagePagerAdapter extends FragmentStatePagerAdapter {
    String[] detailUrls;

    public PagePagerAdapter(FragmentManager fm, String[] urls) {
        super(fm);
        this.detailUrls = urls;
    }

    @Override
    public Fragment getItem(int i) {
        return PageFragment.newInstance(detailUrls[i]);
    }

    @Override
    public int getCount() {
        return detailUrls.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return "" + (position + 1);
    }

}

ダイアログクラスの定義

ここで今回の主役の DialogFragment の登場。

public class PageDialog extends DialogFragment {
    private final static String KEY_URL = "diaog_key_urls";

    public static PageDialog newInstance(String[] urls) {
        PageDialog fragment = new PageDialog();
        Bundle args = new Bundle();
        args.putStringArray(KEY_URL, urls);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.page_dialog, container, false);
        String[] urls = getArguments().getStringArray(KEY_URL);

        final ViewPager viewPager = (ViewPager) view.findViewById(R.id.pager);
        final PagePagerAdapter adapter = new PagePagerAdapter(getChildFragmentManager(), urls);
        viewPager.setAdapter(adapter);

        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        getDialog().setCanceledOnTouchOutside(true);

        return view;
    }
}

レイアウトの生成

layout/page_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v4.view.PagerTitleStrip
            android:id="@+id/pager_title_strip"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:paddingTop="4dp"
            android:paddingBottom="4dp" />
    </android.support.v4.view.ViewPager>

</RelativeLayout>

ダイアログの呼び出し

最後はお好きなタイミングでこれを呼び出すだけだ!

PageDialog newFragment = PageDialog.newInstance(getImagesURL());
newFragment.show(getSupportFragmentManager(), TAG);

Android の EventBus がめちゃくちゃ便利な件について

ライブラリで久々に感動した。これはマジックだ。今回は、greenrobot/EventBus · GitHub を紹介する。

よくアクティビティに独自のコールバックオブジェクトを実装させて、それを他のクラスで渡して実行させる処理がよくあると思う。以下は例。

interface Callback {
  void onFinished();
}

class SampleActivity extends Activity implements Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      HogeProcess process = new HogeProcess(this);
      process.start();
    }

  @Override
  public void onFinished() {
    Log.v("TAG", "finished");
  }
}

class HogeProcess {
  private Callback callback;

  public HogeProcess(Callback callback) {
    this.callback = callback;
  }

  public void start() {
    //.....
    callback.onFinished();
  }
}

概要としてはこんな感じ。割とライブラリとかでも一般的に使われている手法だ。

ただ、このインタフェースを使ったコールバックの方法は、以下のような欠点がある。

  • 毎回コールバックを作るごとにインタフェースを作らないといけない
  • インプリメントがどんどん増えて複雑になっちゃう
  • コールバックオブジェクトをわざわざ管理しないといけない

といった要は煩雑な処理が多くなってしまう。それを解決してくれるのが、このEventBus。

EventBus の使い方

本当に簡単。さっきのをEventBus を使って同じように作ってみる。

class SampleActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      HogeProcess process = new HogeProcess();
      process.start();
    }

    @Override
    protected void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }

    @Override
    public void onStop() {
        EventBus.getDefault().unregister(this);
        super.onStop();
    }

    public void onEvent(HogeEvent event) {
      Log.v("TAG", "finished");
    }
}

class HogeEvent {
}

class HogeProcess {
  public HogeProcess() {
  }

  public void start() {
    //.....
   EventBus.getDefault().post(new HogeEvent())
  }
}

どうでしょう!インタフェースが消えました!それに伴ってHogeProcess内のコールバックオブジェクトも必要ないです。以下に要点をまとめよう。

  • onStart, onStop, 内にregister, unregister をそれぞれ呼ぶ
  • イベントを呼び出したい時に、 EventBus.getDefault().post(イベントクラス) を呼ぶ。この時のイベントクラスの中にフィールドを持たせる事ももちろん可能。
  • 受け取り側で public void onEvent(イベントクラス event) を定義してあげる。このイベントクラス名でかぶっていた場合だけそのイベントをキャッチすることができる。

これだけのルールだ。これだけでかなりソースが簡潔になる。そして無駄な処理が発生しなくなる。今後どのプロジェクトでも使っていきたいと思えるライブラリーだ。

爆速 Android アプリ開発をサポートする Bootroid をOSS化しました

Bootroid は、特に JSON API を通じてサーバとやりとりする Android アプリ開発を爆速化するフレームワークです。 一般的な Web サービスをAndroidアプリで実現しようとした際に有用です。

インストール方法などは上記リンク先を参照していただくとして、この記事ではBootroid の中で特に有用と思われる 3つの主要機能について解説します。

API 通信

HTTP リクエスト

Bootroid 内に Volley が埋め込まれていますので、わざわざ Volley をインストールする必要はありません。ほんの少しのセットアップを済ませるだけで、以下のように簡単にAPIアクセスができます。

        ApiRequest.get(URL, new ApiResponseHandler() {
            @Override
            public void onSuccess(JSONObject jsonObject) {
               // parse json
            }

            @Override
            public void onFailure(ApiException e) {
                // error handling
            }
        });

REST API アクセス

API のレスポンスに対応した Java エンティティを作成するだけで自動でそのフィールドに値が入るような REST API アクセスも可能です。

例えば、以下のようなJSONを返す APIがあったとします。

{
  article: {
    "id": 1,
    title: "Bootroid",
    "author": "honkimi",
    "content": "Bootroid supports your speedy android development."
  }
}

以下のように書くだけで、値の入ったArticleを取得できます!

class Article {
    public int id;
    public String title;
    public String author;
    public String content;
}

private void fetchRestRequest() {
    RestApi.baseKey = "article";
    RestApi.show(URL, Article.class, new ApiCallbackBase.ApiCallback<Article>() {
        @Override
        public void onSuccess(Article response) {
            // article
        }

        @Override
        public void onFailure(String message, int code) {
            // error handling
        }
    });
}

画像表示

HTTP での画像表示

レイアウトは以下のように記述します。

        <com.android.volley.toolbox.NetworkImageView
            android:id="@+id/network"
            android:layout_width="50dp"
            android:layout_height="50dp" />

アクティビティに読み込むURLを指定すればOKです。

NetworkImageView network = (NetworkImageView) findViewById(R.id.network);
String imageUrl = "https://assets-cdn.github.com/images/modules/open_graph/github-mark.png";
network.setImageUrl(imageUrl, ApplicationController.getInstance().getImageLoader());

Fontawesome のアイコン表示

Fontawesome のたくさんのアイコンをAndroidで利用することができます。

レイアウトは以下のように記述します。

    <TextView
        android:id="@+id/github_icon"
        android:text="&#xf092;"
        android:textSize="50sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

&#xf092; は上記リンク先で使いたいアイコンから指定します。

アクティビティに以下を追加するだけです! thisActivity を指します。

IconUtil.setIcons(this, R.id.github_icon);

キャッシュの保存、取得

APIで取ってきたデータを一時的に保管し、次回起動時にそれを読み込ませてオフライン時の対策だったり、ローディングを回避したりといったところでキャッシュは多用されます。

Bootroid ではオブジェクトをそのままJSONに変換し、保存する機能を提供します。

例えば以下のようなエンティティがあるとします。

    class Sample {
        public int id;
        public String name;
        private Sample(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

ListでAPIから取得したとします。つまり、以下のフィールドに値が入っている状態です。

private ArrayList<Sample> samples;
private static final String CACHE_KEY = "sample_cache";

キャッシュの操作

// 保存
ObjectStorage.save(samples, CACHE_KEY);
// 読み込み
Sample[] cache = ObjectStorage.get(CACHE_KEY, Sample[].class);
// 削除
ObjectStorage.remove(CACHE_KEY);

取得した配列をリストに変換し、リスト操作した後また上書き保存することにより、簡単なデータ更新や一部削除なども簡単に実装できます。 複雑なデータ処理はSQLite を使う必要がありますが、シンプルなデータ操作であればキャッシュだけで実現できます。

その他機能

その他にも以下のようなよく使われる機能をまとめてあります。

  • GCM(プッシュ通知) の登録、受け取り処理
  • EditText のバリデーションのフレームワーク
  • Google Analytics 共通処理
  • Splunk 共通処理
  • その他 Androidアプリ開発でよく使う Util系

終わりに

今回公開したソースは全て、実際に作ったプロダクトで外出しにできる部分のうち、特に有用だと判断したもののみを公開しております。

もっと改善してより爆速な Android 開発を目指していきます。

Google Mobile App Developer Panel に参加してみた

今日こんなメールが届いた。

f:id:cevid_cpp:20141120003359p:plain

読んでみると、どうやらAndroid デベロッパー同士のコミュニティーの場のようだ。何かしら有益な情報がメールでくるかもしれないと思い、登録してみた。

これは Google Play に登録している人限定っぽいのでURLは乗っけられないけど、20$払ってGoogle Play デベロッパーになれば誰でも入れる気がする。

実に長いアンケート項目に答えたら、こんな感じのページに入った。

f:id:cevid_cpp:20141120003653p:plain

このコミュニティの一番いいところは、Google の中の人とより近くでコンタクトが取れるということだろう。今まではGoogle に問い合わせたくてもなかなか対応してくれないケースが多かったし、日本のフォーラムで問い合わせても、「開発チームに連絡します」ってだけで具体的な進展があったことはあまりなかった。

もちろん英語必須ではあるがこのようにAndroidデベロッパー向けの公開コミュニティがあることで、今後のAndroid 開発がよりよくなることを期待している。何かAndroid開発での問題が解決できないときはここを利用してみようと思う。

Android でのオブジェクトを保存する便利な方法

Android で端末にデータ保存をする時は以下の選択肢になるかと思う。

  • ファイルとして保存 (SDカードなど)
  • SQLiteデータベースに保存
  • SharedPreference に保存

それら一つ一つに長所・短所があり、使いどころがあるのだけれど、それぞれのメリットを合わせたような素晴らしいやり方があるので紹介。

Gson + SharedPreference

このコンボを利用することだ。具体的にはオブジェクトをJson化して保存し、読み込む時はそのJsonをパースするという方法。これで一気にオブジェクトの保存と復元が可能だ。しかも配列として復元することも可能なのでDBアクセスっぽいこともできる。

さてどうするか。コードを紹介しよう。

保存、取得コードの作成

ObjectStorage.java

public class ObjectStorage {

    public static void save(Object src, String key) {
        String json = new Gson().toJson(src);
        new CachePref().put(key, json);
    }

    public static <T> T get(String key, Class<T> klazz) {
        String jsonStr = new CachePref().get(key, "");
        if (jsonStr.equals("")) {
            return null;
        }
        return new Gson(),.fromJson(jsonStr, klazz);
    }

}

CachePref.java

public class CachePref {
    private final static String RPEF_NAME = "cache";
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;

    public static final String KEY_USER = "user";
    public static final String KEY_USER_LIST = "user_list";

    public CachePref() {
        Context context = ApplicationController.getInstance().getApplicationContext();
        pref = context.getSharedPreferences(RPEF_NAME, Context.MODE_PRIVATE);
        editor = pref.edit();
    }

    public String get(String key, String defaultValue) {
        return pref.getString(key, defaultValue);
    }
    public void put(String key, String value) {
        editor.putString(key, value).commit();
    }
}

これだけだ!

ObjectStorage の利用

以下のようなモデルを用意しよう。

public class User {
    private String id;
    private String email;
    private String password;

    public String getId() {
        return id;
    }
    public String getEmail() {
        return email;
    }
    public String getPassword() {
        return password;
    }
}

そんでそれを読み書きしよう

// 単一
User user;
// userにデータ入れる

// 保存
ObjectStorage.save(user, CachePref.KEY_USER);

// 取得
user = ObjectStorage.get(CachePref.KEY_USER, User.class);


// 配列
User[] users;
// users にデータ入れる

// 保存
ObjectStorage.save(users, CachePref.KEY_USER_LIST);

// 取得
users = ObjectStorage.get(CachePref.KEY_USER_LIST, User[].class);

おわりに

これでSharedPreferenceの良さとDBの良さそれぞれを併せ持つ素晴らしいストレージが完成した。 もちろん、SQLiteに比べてselectを用いたデータアクセスができないという点はあるが、データ数が100個以内なら余裕でforで1件ずつアクセスしても良いと思う。

アップデートも削除も全部上書きすればいいだけなので、データ数の少ないオブジェクトの保存であれば、これで十分だと思う。

コネクシィではこのオブジェクト保存方法でたくさんのキャッシュを生成しております!

Android で Font Awesome を使うのがなかなかいい感じ

Android でアイコン作るときは、まぁ普通は Action Bar Icon Pack とかをよく使うと思う。自分もそれを好んで利用していた。

ただ、これには以下のような欠点がある。

  • 使う画像を利用する度にコピーするがだるい
  • 要領がどんどん大きくなる
  • 種類が少ない

さて、それらを解決してくれるのが Font Awesome を利用したアイコン生成方法だ。

設定方法

まずは Font Awesomeからアイコンパックをダウンロード。

その中の fonts -> fontawesome-webfont.ttf を Android プロジェクトの assets ディレクトリに保存。なければ java とかと同じ階層に作る。

使いたいアイコンを チートシート から探す。

strings.xml に以下を追記

    <string name="icon_fb">&#xf082;</string>

layout はこんな感じ

                <TextView
                    android:id="@+id/fb_icon"
                    android:text="@string/icon_fb"
                    android:textSize="@dimen/text_large"
                    android:textColor="@color/com_facebook_blue"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />

アクティビティ内に以下を記述

        Typeface font = Typeface.createFromAsset(getAssets(), "fontawesome-webfont.ttf");
        ((TextView) findViewById(R.id.fb_icon)).setTypeface(font);

すると、アイコンができる。

Font Awesome にすること

メリットは以下の通り。

  • 種類が多い
  • 容量節約
  • drawable へのコピペは不要
  • 色が自由自在
  • 大きさもdimens.xmlに書けば画面サイズ毎に大きさを変えられる

色変えられるのはかなりでかい!Actionbar icons pack じゃできなかった。

デメリットはこんなのかな

  • 毎回チートシート見てstringsに記入しないといけない
  • activityにコード書かないといけない

所感

アイコンのあるアプリは見た目がいいし、直感的にわかりやすし是非導入したい所。最初からFont Awesomeを利用したAndroidプロジェクトにすることをおすすめします。

自分の場合は一部Actionbar icons pack、一部はFont Awesome となってしまった。。Font Awesomeで統一しようかな〜。

インタフェースを実装したオブジェクトの共通処理をどうにかする

久々にJavaネタ。

interface を用意し、複数のそれを実装したオブジェクトを作ったとき、それぞれ実装したメソッドの中身が似通って困っちゃうってことがあった。その時にどうすればいいかの対応。

interface Outputtable {
  void output();
}

class MyCode implements Readable{
  @Override
  public void output() {
     System.out.println("Hello.");
     System.out.println("bad!");
  }
}

class YourCode implements Readable {
  @Override
  public void output() {
    System.out.println("Hello.");
    System.out.println("good!");
  }
]

Readable readable = new YourCode();
readable.output();

さて、ここで System.out.println("Hello.");が共通処理となってしまった。どうしましょう? そこで今回登場するのが、インタフェースのオブジェクトをフィールドに持つクラス。

class Output {
  private Readable readable;

  public Output(Readable readable) {
    this.readable = readable;
  }

  public void hello() {
    System.out.println("Hello.");
    readable.output()
  }
}

Output output = new Output(new YourCode());
output.hello();

こうすれば、上で書いた重複で書いていた System.out.println("Hello.");は共通化される。

今回はこんな感じのコードを書いたので、残しておこう。デザインパターン的に何かとかは知らん。w

Android の NumberPicker を任意のSTEPでセットするときの対応

NumberPickerを使うたびに詰まってる気がするのでメモ。

NumberPickerで例えばminが500, max が 5000, step が500としたい場合、これがなかなかわかりにくい。メソッドを呼ぶだけで問題が起きないようにリファクタリングした。今後はNumberPickerを使い際はこのメソッドを使う。

    private void setNumberPicker(NumberPicker picker, int min, int max, int step, int format) {
        picker.setMinValue(min / step - 1);
        picker.setMaxValue((max / step) - 1);
        String[] valueSet = new String[max / min];
        for (int i = min; i <= max; i += step) {
            valueSet[(i / step) - 1] = getString(format, i);
        }
        picker.setDisplayedValues(valueSet);
    }

最後の引数 format は, 例えば strings.xml%d円とかしておいて、formatにR.id.format_yen みたいにして指定すると、NumberPickerの各値に反映できる。

ここで注意したいのが、これでpicker.getValue() を呼ぶと、0からの連番の数字になるので、+1してSTEPをかけあわせないといけないことに注意。

Android の Google Plus ログインで SIGN_IN_REQUIRED で内部エラー

久々に発狂しそうなレベルで詰まった。

Google Plus ログインで以下のようなコードを書くことになる。

    @Override
    public void onConnectionFailed(ConnectionResult result) {
        Log.e(TAG, result.toString());
        if (result.hasResolution()) {
            try {
                result.startResolutionForResult(activity, REQUEST_CODE_RESOLVE_ERR);
            } catch (IntentSender.SendIntentException e) {
                googleApiClient.connect();
            }
        } 
     }

さて、hasResolution()では、ユーザがこのアプリへの情報読み取り許可をしていない場合にtrueを返す訳だが、startResolutionForResult() がどうしても呼ばれず、「内部エラーが発生しました」と出て先に進まなかった。

ログを見ると以下のようなメッセージが出た。

onnectionResult{statusCode=SIGN_IN_REQUIRED, resolution=PendingIntent{53363ccc: android.os.BinderProxy@5323fbf0}}

さらに詳細を見ると、

adb -d shell setprop log.tag.GooglePlusPlatform VERBOSE

INVALID_CLIENT_ID my_email@domain oauth2:https://www.googleapis.com/auth/plus.login

答えはここにあった。

Google API コンソールの API & Auth の Consent Screen にちゃんと値をセットしていないと上記のエラーが出る

「内部エラーが発生しました」だけじゃそんなのわかる訳ねぇ!まぁ直って良かった。

Volley の2重リクエストを防ぐ

結構よく起きるので、メモ。

これが起きてしまう原因は、サーバのレスポンス速度が遅いことにある。

サーバ内でメール送信してたりとか(本当はキューに溜めるべきだが)、アクセスが多くなってきたりしたりとかで遅くなると、Volleyはもう一回リクエストを送ってしまうのだ。

対処法

Volley の RetryPolicy を更新してあげればよい。

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        }

        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request<T> req) {
        req.setTag(TAG);
        req.setRetryPolicy(new DefaultRetryPolicy(50 * 1000, 0, 1.0f));
        getRequestQueue().add(req);
    }

これで結構待ってみてくれるようになって、一件落着。

Android の ScrollView で EditText のキーボード非表示にしつつトップに移動する

毎回 ScrollView 付きのフォームを作るときに苦戦するのでメモ。

例えば、保存ボタンがScrollViewの一番下にあり、それを押すと検証が走る。 検証が失敗した時は一番上にエラーメッセージを出したいといった場合。

問題は、途中にあるEditTextのキーボードを非表示にしつつ、フォーカスを外して一番上まで持って行かなければならないという点。途中のフォーカスにひっかかって一番上までいってくれなくて、ハマる。。

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:padding="@dimen/blank_medium"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/account_err_txt"
            android:visibility="invisible"
            android:focusable="true"
            android:focusableInTouchMode="true"
            style="@style/FormParts"
            android:text="Error Messages"
            android:textColor="@color/error" />

....

        <Button
            .....

    </LinearLayout>
</ScrollView>

ここで大事なのが、一番上にあるエラーメッセージのTextViewにフォーカスを当てられるようにすることだ。んでコードを書く。

findViewById(R.id.btn).setOnclickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
         // キーボードを隠す
         InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
         inputMethodManager.hideSoftInputFromWindow(ed.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);   

        // フォーカスを一番上にあるエラーテキストにセット
        errorText.requestFocus();
        // スクロールを一番上へ
        scrollView.fullScroll(View.FOCUS_UP);
    }
}

もっといい方法あるのかな。

Volley + Gson + Generics = God! (Android で OAuth な Rest Api のクライアント作成)

Android アプリでよくあるパターンとしては Restful な Web API を呼んで、リストや詳細を表示などが挙げられる。こんなとき、JSONで通信しているのであれば、リクエストのパラメータを作り、レスポンスを解析するというコードを書く必要がある。これが例外処理やらnullチェックやらで何かと面倒。そう感じることが多かった。 最近、gsonとの出会いがあった。なるほど、これを使えばJSONをパースせずともGetter, Setter を持つモデルクラスを作ればそこに詰め込んでくれる優れものだ。だがもっとシンプルにできないだろうか。。

Gson と Generics の相性

Generics は型に縛られず共通処理を書けるようになるテクニックだ。これを使えば以下のようなシンプルなREST クライアントが作成できる。

色々書いて気づいたが、下記コードを見るだけだととてもわかりにくい。この部分だけ切り出したりできないか検討します。。

public class RestApi {
    public static <T> void index(String url, final Class<T[]> clazz, final ApiCallbackBase.ApiCallback<T[]> callback) {
        ApiRequest.get(url, getListHandler(clazz, callback));
    }

    private static <T> ApiResponseHandler getListHandler(final Class<T[]> clazz, final ApiCallbackBase.ApiCallback<T[]> callback) {
        final Context context = ApplicationController.getInstance().getApplicationContext();
        return new ApiResponseHandler() {
            @Override
            public void onSuccess(JSONObject jsonObject) {
                try {
                    Gson gson = new GsonBuilder()
                            .setDateFormat(context.getString(R.string.date_parse_in)).create();
                    LogUtil.d(jsonObject.getJSONArray("item").toString());
                    T[] dtoList = gson.fromJson(jsonObject.getJSONArray("item").toString(), clazz);
                    if (callback != null) {
                        callback.onSuccess(dtoList);
                    }
                } catch (JSONException e) {
                    onFailure(new ApiException(e.getMessage(), ApiException.JSON_PARSE_ERROR));
                }
            }

            @Override
            public void onFailure(ApiException e) {
                LogUtil.e("Index Failed..");
                LogUtil.e("msg:" + e.getMessage() + ", status: " + e.getStatusCode());
                if (callback != null) {
                    callback.onFailure(e.getMessage(), e.getStatusCode());
                }
            }
        };
    }

}

ApiRequest で Volley のhttp 通信を行なう。

public class ApiRequest {

    public static void get(String url, final ApiResponseHandler handler) {
        try {
            request(Request.Method.GET, url, null, handler);
        } catch (JSONException e) {
            handler.onFailure(new ApiException(e.getMessage(), ApiException.JSON_PARSE_ERROR));
        }
    }

    private static void request(final int method, final String url, final JSONObject params, final ApiResponseHandler handler) throws JSONException {
        RequestDto reqDto = new RequestDto(method, url, params);
        reqDto.setAccessToken();
        JsonObjectRequest req = new JsonObjectRequest(reqDto.method, reqDto.url, reqDto.params,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        handleSuccessResponse(response, handler);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError e) {
                LogUtil.e("Error: " + e.getMessage());
                e.printStackTrace();

                int status = ApiException.UNKNOWN_ERROR;
                if (e.networkResponse != null) {
                    status = e.networkResponse.statusCode;
                }
                if (status == 401) {
                    if (params != null && params.has(TokenParamDto.GRANT_REFRESH_TOKEN)) {
                        SignupActivity.redirectToSignup();
                    } else {
                        tokenRefresh(method, url, params, handler);
                    }
                    return;
                }
                try {
                    String responseBody = new String(e.networkResponse.data, "utf-8");
                    JSONObject jsonObject = new JSONObject(responseBody);
                    LogUtil.e(jsonObject);
                } catch (Exception e2) { }

                handler.onFailure(new ApiException(e.getMessage(), status));
            }
        }
        );
        ApplicationController.getInstance().addToRequestQueue(req);
    }

抜粋ではあるが、OAuthのHttpクライアントとしてリフレッシュトークン、アクセストークンのやりとりを実装するには自前でやるのが一番早い。これらを必要としないシンプルなREST Apiであれば、Retrofit などのOSSを使うことも検討できるだろう。ただ、中身がVolleyじゃなくなるのでそこら辺、自分としては気持ち悪いところ。そこまでこだわりなければ全然使っていいと思う。