Android ライブラリの書き方

ある時点で、開発者は特定のタスクや、コードをモジュール化できないか、エレガントな方法でコードを再利用できないかなどを考え、ライブラリが作れないかどうか考えると思います。しかし、実際にライブラリを書くのは大変な仕事です。Bay Android Dev Group で Realm の開発者である Emanuele Zattin が Java と C/C++ を含むライブラリを書く上でのベストプラクティスについて発表を行いました。API デザイン、CI のテクニック、パフォーマンスの向上などについて普段の仕事で役に立つ情報であると思います。


Android ライブラリを書く理由 (0:00)

ライブラリを作る理由として最初に挙がるのはモジュール性です。コードをよりクリーンで管理しやすくするためにいくつかの論理的なユニットに分割したいことかと思います。そして、二番目の理由は、再利用性です。コードをモジュール化することで、他の場所で再利用することができるようになります。また、コードが一箇所に集まっているため一枚岩のコードである時よりも簡単に変更を加えれるようになります。またそれらに加え、他の理由として “虚栄心” もあると思います。特定のタスクを行う良い方法を思いついた場合、それをライブラリにすることで世の中に共有することができ、多くの人が使えるようになります。

なぜ Java ライブラリではなく Android ライブラリなのでしょうか? もし純粋な Android を扱っているのなら、選択の余地はありません。また UI や メッセージシステム、デバイスのセンサーやネイティブコードを扱っている場合も、Android ライブラリを開発しなければいけません。

Step 1: はじめに (3:02)

まず、Android Studio を起動し、新しいプロジェクトを作成します。しかし、残念ながら Android Studio はライブラリの作成をサポートしていません。ライブラリを作成しようとすると少しの手間が必要となります。一つの選択肢として、新しいアプリケーションプロジェクトを作ります。そして、ライブラリモジュールを追加し、アプリケーションモジュールを削除します。ちょっとした Hack ですが、単純で問題なく上手くいくやり方です。

また、他の方法としてコマンドラインツールを使う方法があります。Android のコマンドはたくさん便利な機能があり、ライブラリプロジェクトを作成する機能がその中にあります。オプションを指定することで、ターゲットの ID やパッケージ名、Gradle プロジェクトが必要かなどを指定できます。Gradle は、かなり強力で柔軟なオートメーションビルドツールです。Plugin を使うことで Maven や ANT よりも簡単に導入ができ、Gradle を使うことで開発がより簡単になります。

Step 2: Code, code, code! (5:47)

ライブラリを作る場合、API のデザインはとても重要なことです。Joshua Bloch はこの辺りのことでかなり有名な人です。彼の著書の Effective Java は Java 1.5 に関することですが、しかし今でも価値のある情報です。また “How to Design A Good API and Why it Matters” というプレゼンがあります。その発表の中で気になったところについてここで紹介したいと思います。

API を良くする要因とは一体何でしょうか?

  • API は理解しやすいものでなければなりません。誰もが開発時に毎回ドキュメントを確認したくないと思います。メソッド、クラス、引数などの名前は可能な限り分かりやすくする必要があります。
  • API の使い方の間違いを防ぐために、なるべくしっかりとしたものでなければなりません。
  • 当分の間、そのコードを使うことになると思いますし、特に、他のユーザーの間で使われるようになるとフィーチャリクエストやバグレポートがあるためコードをできるだけ読みやすく、メンテナンスしやすくしておくべきです。
  • API はあなた自身の要求を満たすものでなければいけません。時々、ユーザーからあなたのデザインや要求に反するリクエストを受けるときがあると思います。ですが、それは実装しないと言える勇気を持つことが必要です。
  • また、API は拡張しやすい作ることが重要です。Jenkins について考えてみてください。API ではないですが、とても成功したオープンソースプロジェクトと言えるでしょう。Jenkins が成功した理由の一つは、プラグインが書け、容易に拡張ができたからです。
  • 最後に、誰に向けて作っているのかはっきりさせる必要があり、その API はその人たちにとって適切か判断してください。それが、自分自身か、あなたのチームか、会社か、はたまた世界中の人々かもしれません。

Step 3: テスト (8:55)

テストをすることは何事においても重要なことです。しかし、ライブラリの場合は、作者が想像もしないような使い方をユーザーはするので、より重要になってきます。幸運なことに、Android のライブラリのテストは Android アプリのテストとほとんど同じです。Android のテストケースが使え、 Android フレームワークが提供する全ての機能が使えます。

Android のテストで良くない点は、Robolectric のようなツールが使えない場合は、JUnit 3 に頼らざるを得なくなるところです。JUnit3 で不足していることは、パラメーターを使ったテストができないところです。ライブラリのテストを行う場合、出来るだけ多くのパラメータでメソッドのテストを行いたいと思うことがあるのでとても大事なことです。またその場合は Square 製の Burst という素晴らしいライブラリがあり、その機能を完璧に提供しています。

テストは自動化してください! Jenkins はこれを行う便利なツールです。1000個以上のプラグインがあり、 Android 開発向けのものもたくさんあります。特に以下のプラグインをオススメします。

  • JobConfigHistory Plugin は、設定ファイルで設定が行え、壊れた時にすぐに戻すことができるようになります。
  • Git Plugin は、Gitを扱うプラグインで他にも GitHub、GitHub Pull Request、GitLab など様々なものがあります。
  • Gradle Plugin はタスクの実行を簡単にし、それを自動で実行してくれます。Gradle はとても優れた自動化ツールで、Jenkins の代わりに Gradle の中で様々なロジックを走らせることができるようにしてくれます。
  • Android Emulator Plugin は、実際エミュレーション以上のことが行えます。異なった解像度のスクリーンや、メモリの状態をテストするときなどに非常に便利なツールです。

またライブラリのテストを行う別の方法としては実際にテストアプリを書いてみることです。サンプルアプリを作ってみることでユースケースがわかったり、アプリでクラッシュしたりしないか確認することができます。これを行うために、gradle-android-command-plugin とういうプラグインがあります。これは、1つ以上の実機にコマンドを送ることができるようになります。

Step 4: 公開 (13:58)

そして、ライブラリを公開するときには二つ選択肢があります。Jar と Android アーカイブの Aar があります。Aar は Google が考え出した、フォーマットです。Java のクラスのみではなく、UI 要素のライブラリを作れるようにするためにアセットやリソースも含むことができます。Android と Android Studio ではサポートされていますが、Ant と Eclipse ではサポートされていません。

Aar に関して他に不便な点は、ローカルの Aar ファイルを参照するには2つの方法がありますが、どちらも容易ではないということです。これは特にテストアプリで重要となってきます。なぜなら、ビルドプロセスで生成した Aar を使いたいからです。そして、最終的には Eclipse のサポートをしたいかどうかになります。する必要がある場合は、Aar ではできないので、Jar を使うしか方法はありません。

問題は Android の Gradole Plugin は Jar ファイルではなく、Aar ファイルを生成することです。しかし、Aar ファイルの中には Jar ファイルが含まれているので、それをコピーし、リネームするだけで良いです。以下のコードは、Gradle で Aar から Jar ファイルを抜き取るコードです。Jar ファイルは Aar の中では必ず classes.jar という名前になっているので、それをコピーし、好きな名前にリネームしています。

task generateJar(type: Copy) {
    group 'Build'
    description 'blah blah...'
    dependsOn assemble
    from 'build/intermediates/bundles/release/classes.jar'
    into 'build/libs'
    rename('classes.jar', 'awesome-library.jar')
}

また、どのように公開するかが大事になります。特にオープンソースとして公開するのなら、これといって考える必要があることはありません。Bintray はとても���晴らしいツールです。少しのステップは必要ですが、とても簡単に公開することができます。アップロード時には javadoc.jarsources.jar も必要となります。

// sources Jar
task androidSourcesJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs 
}

// Javadoc Jar
android.libraryVariants.all { variant ->
    task("javadoc${variant.name.capitalize()}", type: Javadoc) {
        description "Generates Javadoc for $variant.name."
        group 'Docs'
        source = variant.javaCompile.source
        ext.androidJar = files(plugins
                                .findPlugin("com.android.library")
                                .getBootClasspath())
        classpath = files(variant.javaCompile.classpath.files) +
                    ext.androidJar
        exclude '**/BuildConfig.java'
        exclude '**/R.java'
    }
}

Bintray の素晴らしい Gradle プラグインがあります。しかし、特に Gradle 初心者にはとっては使うのが少し難しいかと思います。一方で、Web インターフェイスはかなり簡単に使えます。3つのファイルをアップロードし、いくつかの情報を入力するだけで完了です。Bintray にあまり馴染みがない人ははじめは Web インターフェイスを使い、その後、自身が増してきたところで、Gradle Plugin に手を出し、より自動化してみるのが良いのではないかと思います。Jenkins でのリリースでも同様のことが言えます。

Advanced Topics

アノテーションプロセッサ (19:28)

アノテーションプロセッサは最近、特に注目を集めている技術です。コンパイル時に javac が定義されたアノテーションを探し、クラスや Java ファイルを生成します。また、簡単という訳ではないですが、独自のアノテーションプロセッサを書くこともできます。

アノテーションプロセッサを使うことには2つメリットがあります。一つはボイラープレートなコードを減らすことができます。また、二つ目として、ランタイムからイントロスペクションを行うコードも減らすこともできます。アプリケーションでは、ランタイム時よりもコンパイル時により多くのことを解決した方がスピードが速くなります。Dagger, Butter Knife, AutoValue/Autoparcel, Realm などがアノテーションプロセスを上手く使っているライブラリです。

また悪い点として Android API にはアノテーションを行うパッケージがありません。これを解決するために、二つの純粋な Java のサブプロジェクトを作ります。一つがアノテーションのためのもので、もう一つがアノテーションプロセッサのためのものです。アノテーションにはプロセッサとコードの両方が必要となります。Android ライブラリプロジェクトのサブプロジェクトを作ることになります。そして、クラスだけでなく、アノテーションとアノテーションプロセッサを含めるように Jar ファイルに変更を加えます。また、ドキュメントを生成するためにアノテーションを加えるのに javadoc のタスクにも変更を加えます。

// Jar
task androidJar(type: Jar) {
    dependsOn assemble
    group 'Build'
    description 'blah blah'
    from zipTree(
        'build/intermediates/bundles/release/classes.jar')
    from zipTree(
        '../annotations-processor/build/libs/processor.jar')
    from zipTree(
        '../annotations/build/libs/annotations.jar') 
}

// javadoc tasks
android.libraryVariants.all { variant ->
    task("javadoc${variant.name.capitalize()}", type: Javadoc) {
        description "Generates Javadoc for $variant.name."
        group 'Docs'
        source = variant.javaCompile.source
        source "../annotations/src/main/java"
        ext.androidJar = files(plugins
                                .findPlugin("com.android.library")
                                .getBootClasspath())
        classpath = files(variant.javaCompile.classpath.files)
                    + ext.androidJar
        exclude '**/BuildConfig.java'
        exclude '**/R.java'
    } 
}

ネイティブコード (25:10)

ネイティブコードは NDK に関わるものです。これの使い方について説明するには、丸々ワークショップが必要になるようなものですが、もし C/C++ に関して馴染みがあるのであれば、扱いやすいものだと思います。Gradle と Android Studio は完全には NDK の対応ができていません。NDK モジュールを使うと、NDK サポート非推奨の警告が出るかと思います。現在できるだけ早くサポートをしているようですが、Gradle のネイティブコード用のプラグインを使うようにしてください。それが対応されるまでは、手動の設定ツールチェーンに頼り、手動でコンパイルし、正しい場所にファイルを移動させる必要があります。

それではどうしましょうか? 一つの解決策として、それは動いてはいるので警告を無視することです。一つ気をつけるべき問題は、ldFlags の定義がないことです。よって、リンカのためのフラグを特定することができません。フラグが必要なときは、別の解決策である Native プラグインを使うことです。

Jar ファイルを使っている場合、ネイティブライブラリをどのように取り組めばいいでしょうか? それはビルド時のことなので取り組むためには、以下のような “jar” タスクを追加することで行えます。

task androidJar(type: Jar, dependsOn: ['assemble']) {
    group 'Build'
    description 'blah blah'
    from zipTree('build/intermediates/bundles/release/classes.jar')
    from(file('src/main/jniLibs')) {
        into 'lib'
    }
}

重要なポイント + Q&A (30:08)

  • Gradle を使う: Gradle の使い方を取得するには少し時間がかかりますが、非常に価値のあることです。
  • Gradle プラグインを探す: Gradle プラグインはたくさんあります。そして、だいたいの場合、求めているようなプラグインがあると思います。
  • テストの自動化: Jenkins を使い可能な限り自動化してください。
  • Bintray は良い解決策: オープンソースである場合、公開するにあたって、かなり簡単に行えるので非常に良い方法だと思います。

Q: Jenkins で Gradle プラグインを使うメリットは何ですか?
Emanuele: Gradle を動作させる方法はいくつかあります。しかし、プラグインを使うことで、何か問題があったときにログを見ることがより簡単になります。

Q: アノテーションはコンパイルタイムに多くのことができるので良いと言われていましたが、ランタイム時に関しては何かありますか?
Emanuele: ランタイムでも同じようにアノテーションが使えます。イントロスペクションなどが使えますが、コンパイル時にするよりもかなり遅くなります。Android で���かなり遅いです。なので、ラインタイムでのアノテーションをあまり使いたいとは思わないと思います。

Q: とても役立つライブラリがたくさんあります。どれも Android でパフォーマンスに問題なく動いていますか?
Emanuele: 私が単に言いたいのは、コンパイル時にイントロスペクションができるのであれば、そうすべきだということです。時にランタイムでのみ必要な情報が取得できないときがあります。そういったときにはランタイムに行うしか仕方がありません。

Q: Javapoet は、アノテーションプロセスととても役に立つライブラリです。しかし、アノテーションやクラスのヒエラルキーを読むのはとても面倒です。何か良い方法を知っていますか?
Emanuele: 残念ながら知りません。javapoet は新しいクラスを追加するのに役に立つものです。もし、あまり多くのコードを書きたくないのであれば、templates が使えまし、そうすれば追加のプラグインを使う必要がなくなります。問題は、イントロスペクションには制約がたくさんあることです。クラスや、クラスのアノテーションなどを取得することはできますが、メソッドそれ自体をイントロスペクションすることはできません。それを解決するためには、少し恐いですが、バイトコードを改ざんすることで可能になり、Morpheus というライブラリが役に立つかと思います。

Q: ライブラリを作っていたとき、アプリではなく、ライブラリを作っていることを明白にするために、正しくGradle の設定をする必要がありました。今までこのような経験をされたことはありませんか?
Emanuele: はい、Google Android Gradle プラグインを使う必要があります。必要な情報がどこにあるのか示し使用する必要があります。

Q: どの Jenkins プロバイダが好みですか? それとも常に自分のローカルで動かしていますか?
Emanuele: はい、自分のマシンで動かしています。マスターではジョブの実行は行わず、スレーブをいくつか用意し、そこで実行するようにしています。そうすることでマスターはかなり小さなマシンで良く、パワフルなマシンである必要がなくなります。



Emanuele Zattin

Emanuele Zattin

Emanuele is a Java, CI and tooling specialist at Realm. Previously, during the good ol’ golden years at Nokia, he helped the whole company unify under one CI system (Jenkins) and switch their version control system to Git. He's an active member and speaker within the Jenkins community where he contributes to the core and maintains several plugins.