こんにちは。 「From A navi(フロム・エー ナビ)」のアプリ開発チームの元です。
iOSやAndroidアプリを多数開発しており、現在は主にAndroid開発を担当しています。
この記事ではAndroidアプリ開発で定番とも言えるOSSライブラリをご紹介します。そしてOSS導入時の注意点についてもお話したいと思います。

Android OSSの事情

少し前の時代の話になりますが、Android向けのOSSは決して活発とは言えませんでした。
その原因の一つとして、メーカーのOSバージョンアップ対応がまちまちだったことがあります。そしてOSバージョンの断片化が激しくなった結果、あるバージョンに対応したソースコードだけではプロダクトとして動作を保証するのが難しくなってしまいました。
それゆえにAndroid向けOSSライブラリへの信頼も低く、さらにそこまで実績がある(ノウハウが詰まった)OSSは少なかったのです。

数年前、私も微力ながらAndroid向けORMライブラリを出した時がありますが、当時Android ORMは私のものを合わせてもたった二つしかありませんでした。
しかし、今はAndroid開発にOSSは当たり前な時代です。SDKの安定化やGoogleのSupport Library群のおかげもあって、断片化によるOSS動作問題などがほぼ解決されました。

そしてGitHubの普及やそこに現れた実力者グループ(Square等)の登場も、Android向OSSの勢いに大きく貢献しています。
合わせて新しいビルドツール(Gradle)の登場がJava特有のライブラリ導入コストをグンと下げています。
もはやAndroid開発する時はSDKはもちろんのこと、公式・3rd Partyライブラリの確認から行うのが一般的になりました。

それでは、これからAndroid開発によりも役立つOSSをご紹介します。

これからも使えるAndroid向けOSS

ButterKnife

AndroidのView InjectionライブラリであるButterKnifeは、ViewのfindViewByIdをなくせるOSSです。
ActivityやFragment等、変数として利用するViewが10個あったとしたら、従来では10個すべてにfindViewByIdのマッピング処理を書かないといけませんでした。これを使うとViewやStringリソース、Clickイベント等がアノテーション定義だけで済むので、コード量削減と可読性が向上します。

従来のコード

private TextView captionTextView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_image_detail, container, false);
    // captionTextViewをbind
    captionTextView = (TextView)view.findViewById(R.id.label_caption);
    captionTextView.setText(media.getCaption().getText());
    .
    .
    省略
    .
    .
}

Butterknifeを使った場合

@Bind(R.id.label_caption)
TextView captionTextView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_image_detail, container, false);
    // ButterKnifeへbind
    ButterKnife.bind(this, view);
    // captionTextViewをそのまま利用可能
    captionTextView.setText(media.getCaption().getText());
    .
    .
    省略
    .
    .
}

Picasso

PicassoはImageViewの非同期ダウンロード処理やリサイクル等、複雑な画像取得・表示処理を簡単にしてくれるOSSです。
内部ではメモリとファイルキャッシュの仕組みが組み込まれているので、処理パフォーマンスにもつながります。
また、カスタムダウンローダーやインターセプトもサポートしているので拡張も簡単です。
例として、RecyclerView.Adapterでは以下のような使い方ができます。非同期で成功した時だけImageViewを表示することにしたコードです。

@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        .
        省略
        .
        Picasso.with(context).load(item.getImages().getThumbnail().getUrl())
            .error(R.drawable.noimage).into(imageViewHolder.itemImageView, new Callback() {
                @Override
                public void onSuccess() {
                    imageViewHolder.itemTextView.setVisibility(View.VISIBLE);
                }
                @Override
                public void onError() {
                }
        });
        .
        省略
        .
}

Retrofit

非常に強力なAndroidのREST OSSです。
従来の方式でAPI通信処理を書いた方はご存知だと思いますが、そのコード量とテストすべき部分は想定を超える量になりがちでした。さらにAPIの変更対応になると完全力仕事になります。Retrofitを使えば、そのリクエストとレスポンスの定義をInterfaceとして用意するだけですべてが済みます。
また、マッピングやHttpClinetのカスタマイズができるようになっているので拡張性も抜群です。
以下の例は、Instagramから特定tagの画像リストを取得する処理です。RecentByTagはAPIのレスポンスをマッピングするオブジェクトです。

// APIのリクエスト定義
public interface InstagramService {
    @GET("/tags/{tag_name}/media/recent")
    RecentByTag getRecentByTag(
            @Path("tag_name") String tagName,
            @Query("access_token") String accessToken,
            @Query("min_id") String minId,
            @Query("max_id") String maxId
    );
}

// レスポンスDTO
public class RecentByTag implements Serializable {

    private Pagination pagination;

    private List<media> data;

    public Pagination getPagination() {
        return pagination;
    }

    public void setPagination(Pagination pagination) {
        this.pagination = pagination;
    }

    public List</media><media> getData() {
        return data;
    }

    public void setData(List</media><media> data) {
        this.data = data;
    }
}

// 非同期処理として書く
RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(getString(R.string.api_endpoint)).build();
InstagramService service = restAdapter.create(InstagramService.class);
RecentByTag recentByTag = service.getRecentByTag(title, InstagramService.apiToken, null, null);

Realm

RealmはORM OSSです。
従来のORMはほとんどが単なるSQLiteOpenHelperの拡張ライブラリに過ぎなかったとも言えますが、Realmは独自のモバイルDBとしてSQLiteより様々な面で優れています。
少し前まではマルチスレッドと絡んだ処理に若干の不具合がありましたが、最新バージョンでは(おそらく0.87以降)綺麗に解決されています。
歴史の長いORM OSSであるActiveAndroidに慣れているユーザであれば、すぐ使えるほどシンプルな仕様をもっているのも特徴です。

Realm realm = Realm.getInstance(getActivity().getApplicationContext());
// Select
RealmResults<tag> tags = realm.where(Tag.class).findAllSorted("createdDate", true);

// Realmオブジェクト
public class Tag extends RealmObject {

    private String id;

    private String title;

    private Date createdDate;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }
}

Retrolambda

RetrolambdaとはJava8からサポートしているラムダ式をレトロJava(7~5)で使えるようにしたOSSです。
マジックのようにも見えますが、中ではJava8でコンパイルされたバイトコードをレトロな環境向けに変換するだけです。
また、Java8のMethod Referenceもサポートしているので、setListener系はより綺麗になります。
以下の例では、Adapter生成時にイベントのコールバックメソッドを引数として渡しています。また、onRefreshをMethod Referenceとして渡しています。

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        .
        省略
        .
        swipeRefreshLayout.setOnRefreshListener(this::onRefresh);
        recyclerView.setAdapter(new ImageRecyclerAdapter(getActivity(), imageDataList, position -> {
            Fragment fragment = ImageDetailFragment.newInstance(imageDataList.get(position));
            getActivity().getSupportFragmentManager().beginTransaction().addToBackStack(null).add(R.id.group_root, fragment).commit();
        }));
        .
        省略
        .
}

RxJavaRxAndroid

RxJavaは名前通りのJava Reactive Extensionsです。そして、RxAndroidはRxJavaにAndroid向けの便利なクラスやメソッドが幾つか追加されたものです。

RxJavaでは存在しないAndroidSchedulers.mainThread()などがRxAndroidの部分です。
例では、先ほど紹介したInstagram APIの戻り値をRxJavaインターフェースに合わせてObservableにしています。それによってRetrofitの非同期処理を簡単にRx化できるようになります。

public interface InstagramService {
    @GET("/tags/{tag_name}/media/recent")
    Observable<recentbytag> getRecentByTag(
            @Path("tag_name") String tagName,
            @Query("access_token") String accessToken,
            @Query("min_id") String minId,
            @Query("max_id") String maxId
    );
}

RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(getString(R.string.api_endpoint)).build();
InstagramService service = restAdapter.create(InstagramService.class);
service.getRecentByTag(title, InstagramService.apiToken, null, null)
    // バックグラウンドで通信実行
    .subscribeOn(Schedulers.newThread())
    // 結果処理はメイン(UI)スレッドで
    .observeOn(AndroidSchedulers.mainThread())
    // subscribeを登録
    .subscribe(new Observer</recentbytag><recentbytag>() {
            @Override
            public void onCompleted() {
                if (recyclerView != null && recyclerView.getAdapter() != null) {
                    recyclerView.getAdapter().notifyDataSetChanged();
                    if (isRefresh) {
                        swipeRefreshLayout.setRefreshing(false);
                    }
                }
            }

            @Override
            public void onError(Throwable e) {
                LogUtil.e(LogUtil.exceptionToString(e));
            }

            @Override
            public void onNext(RecentByTag recentByTag) {
                List<media> medias = recentByTag.getData();
                imageDataList.addAll(medias);
            }
});

Design Support Library

Design Support Libraryは、2015年発表されたGoogleサポートライブラリ群の一つです。
主要な役割としては、Android 5.0以上だけのものであったマテリアルデザインコンポーネントを、Android 2.1以降からも利用可能としました。
これまではマテリアルデザインをサードパーティーOSSに依存していましたが、このDesign Support Libraryによってほぼ100%公式ライブラリだけで実現できるようになりました。

サンプルアプリ

上記でご紹介しているOSSを利用してサンプルアプリを作りました。画像をカテゴリ(タグ)ごとにタブ化して表示するアプリです。
タブはキーワードごとに追加や削除ができます。また、各タブごとに引っ張ってリフレッシュできます。
Android01

ソースコードはGitHubに公開しているのでぜひ動かしてみてください。
※動作にはInstagram APIのtokenが必要です。
https://github.com/wonhyeongju/image-viewer-sample

OSSを導入する時の注意点

リスクとメリットについて考慮しよう

紹介したOSS以外にも便利なものはたくさんあります。また、明日には新しくてより優れたOSSが流行り始めているかもしれません。そして、それを早く使ってみたいと思うのは、アプリエンジニアとしてはごく自然なことです。
しかし、OSSライブラリが必ずしもコスト削減や課題解決につながるとは限りません。
当然のことながら、導入する時にはあらゆるリスクと本質的なメリットを考慮すべきです。

まず、知識として実現する実装イメージが立っていないままOSSを使うのはリスクの高い行動です。
あるプロダクトにおいて実装コスト削減のためにとりあえず簡単に導入・実現できるOSSを導入したとします。
そして、もしOSSと絡んでいる部分から不具合が発生した場合は、対応コストが削減されたコストより高くなってしまうかもしれません。
最悪の場合はOSSにした部分を諦めることになり、その部分を書き直す可能性も出てくるでしょう。
従って、実装コストを削減する目的の場合は、あらかじめ実装イメージを持ち、その一部分(または全体)をOSSで解決することを推奨します。

UI系のOSSは拡張性の有無がポイント

次に、UI系OSSは、アプリのコンセプトとかけ離れて邪魔になってしまう可能性もあります。
独自のUI/UXが充実したアプリの場合は、OSSのカスタマイズを前提に導入する必要があります。
アプリはUI/UXへの仕様変更・追加が激しい分、いずれまたは近いうちにその部分に手を入れることになるのは必至です。
そして、そのOSS部分の拡張が難しくなった時に、自作するより拡張コストが高くなることもたびたびあります。
従って、UI系のOSSを導入する前には、必ずある程度コードを読んで拡張性を把握しておくことが望ましいでしょう。

SDKラッパー系OSSは競合に注意!

最後に、既存機能と競合してしまい、思ってもいなかった箇所からデグレが発生する可能性もあります。
OSSの中には、Android SDKですでに提供している機能(クラス)をラップしているものも多くあります。
そのため何かしらの箇所で既存実装部分とぶつかり、想定以外の結果を起こす場合もあります。
例えばORM OSSであるActiveAndroidを導入した場合、当然既存バージョンのデータを引き継ぐための仕組みを考慮しないといけません。
結果、既存SQLiteOpenHelperで実装した部分とのアクセスタイミングで競合する可能性が高くなります。
従って、SDK拡張系のOSS導入時には、その仕組みをしっかりと理解した上で使うべきです。

終わりに ~Android開発の鍵になったOSS~

アプリの仕様が徐々に複雑化している中、実装コスト削減はAndroid開発者の宿命になっています。
そして、もはやOSS無しでのAndroidアプリ開発はあり得ない状況にまでなってきたと思います。
必要な場面に適切なOSSを導入して課題を技術的にクリアしていく努力が今後大事になっていくでしょう。</media></recentbytag></tag></media>