はじめに
Android界隈で最近aptを使ったライブラリが増えてきていると感じています。しかしapt自身、特にAndroidでの書き方を解説している日本語の記事は多くありません。ライブラリを書かないとしても、どのように動くかを知っておくことでエラーに対応しやすくなったり、プルリクを送りやすくなったり良いことがあると思うので、私の経験を踏まえてaptのライブラリを作るときの知見を共有したいと思います。
apt(Annotation Processing Tool)とは
Jake WhartonのDroidconのスライド(Annotation Processing Boilerplate Destruction)を見てくれという感じなのですが、枚数が多いので簡単と説明すると、Javaはコンパイル時に処理を割り込む仕組みがあり、そのときに渡されるソースコードのメタデータの塊を操作してコードを生成したりすることができます。ある言語におけるマクロを定義しておくとコンパイル時にASTが入ってきてコードをいじれるみたいな感じです。
aptはコンパイル時に実行されるので、リフレクションと比べると型安全で実行時のオーバーヘッドがないことが知られています。そのためリフレクションを使って書かれたコードがのちほどaptで書き直されるということが稀によくあります。
aptを使ったライブラリ
aptを使っていて有名なライブラリと言えば Dagger や ButterKnife ですが、これらはmavenを使っています。Androidアプリを開発している多くの開発者はgradleを使っていると思うので、gradleのプロジェクトで参考になりそうなものをいくつかピックアップしました。
aptを使ったライブラリを作る
プロジェクトの構成
aptを使ったライブラリの多くはコンパイル時に処理を行うモジュールと、アプリから参照するモジュールの2つが定義されています。たとえば上記のライブラリでは以下のような構成になっています。
| コンパイル時 | アプリから参照
:---|:---|:---
IntentBuilder | compier | api
flender | plugin | runtime
DeepLinkDispatch | processor | -
DBFlow | compiler | -
-
はプロジェクト名がそのままモジュール名になっているという意味で、DeepLinkDispatchであれば
apt 'com.airbnb:deeplinkdispatch-processor:x.y.z'
compile 'com.airbnb:deeplinkdispatch:x.y.z'
このようになります。好みの問題ではありますが、私はこの ライブラリ名
と ライブラリ名 + processor
のペアが一番自然かなと思います。
プロジェクトを始めるときは、まずAndroidアプリケーションを選んで、作られたappモジュールをリネームして動作確認用のサンプルアプリにして、aptを行うためのJavaライブラリと、アプリから使うためのAndroidライブラリの合計3つのモジュールを作ります。
// settings.gradle
include ':sample' // サンプルのAndroidアプリ
include ':processor' // aptのJavaライブラリ
include ':library' // Androidライブラリ
Processorを定義する
javacがフックできるようにProcessorのエントリポイントを定義します。main/resources/META-INF/services
に自分で定義する方法と、AutoService というライブラリを使って自動生成する方法があります。私はAutoServiceを使っていますが、大した作業ではないのでどちらでもいいと思います。
ちなみにAutoService自体もaptでマニフェストを生成しています。このようにaptはJava以外のファイルを生成することもできます。
Processorの定義はこのようにします。
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
...
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
...
}
}
processメソッドにRoundEnvironmentというメタデータの塊が入ってくるので、そこからアノテーションを見てフィールド、メソッド、クラスを取得して処理します。
Javaコードを生成する
Javaのコード生成にはsquare/javapoet を使うのが主流だと思います。以下のようにフィールド、メソッド、クラスを定義してファイルに書き出します。
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
基本的にはこれだけです。
gradleの小ネタ
Bintrayへのアップロード
BintrayへライブラリをアップロードするときにBintrayの公式のライブラリである bintray/gradle-bintray-plugin を使うと以下のようにする必要があります(READMEより)。
bintray {
user = 'bintray_user'
key = 'bintray_api_key'
configurations = ['deployables'] //When uploading configuration files
// - OR -
publications = ['mavenStuff'] //When uploading Maven-based publication files
// - AND/OR -
filesSpec { //When uploading any arbitrary files ('filesSpec' is a standard Gradle CopySpec)
from 'arbitrary-files'
into 'standalone_files/level1'
rename '(.+)\\.(.+)', '$1-suffix.$2'
}
dryRun = false //Whether to run this as dry-run, without deploying
publish = true //If version should be auto published after an upload
//Package configuration. The plugin will use the repo and name properties to check if the package already exists. In that case, there's no need to configure the other package properties (like userOrg, desc, etc).
pkg {
repo = 'myrepo'
name = 'mypkg'
userOrg = 'myorg' //An optional organization name when the repo belongs to one of the user's orgs
desc = 'what a fantastic package indeed!'
websiteUrl = 'https://github.com/bintray/gradle-bintray-plugin'
issueTrackerUrl = 'https://github.com/bintray/gradle-bintray-plugin/issues'
vcsUrl = 'https://github.com/bintray/gradle-bintray-plugin.git'
licenses = ['Apache-2.0']
labels = ['gear', 'gore', 'gorilla']
publicDownloadNumbers = true
attributes= ['a': ['ay1', 'ay2'], 'b': ['bee'], c: 'cee'] //Optional package-level attributes
//Optional version descriptor
version {
name = '1.3-Final' //Bintray logical version name
desc = //Optional - Version-specific description'
released = //Optional - Date of the version release. 2 possible values: date in the format of 'yyyy-MM-dd'T'HH:mm:ss.SSSZZ' OR a java.util.Date instance
vcsTag = '1.3.0'
attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin'] //Optional version-level attributes
//Optional configuration for GPG signing
gpg {
sign = true //Determines whether to GPG sign the files. The default is false
passphrase = 'passphrase' //Optional. The passphrase for GPG signing'
}
//Optional configuration for Maven Central sync of the version
mavenCentralSync {
sync = true //Optional (true by default). Determines whether to sync the version to Maven Central.
user = 'userToken' //OSS user token
password = 'paasword' //OSS user password
close = '1' //Optional property. By default the staging repository is closed and artifacts are released to Maven Central. You can optionally turn this behaviour off (by puting 0 as value) and release the version manually.
}
}
}
}
このような感じで長くなってしまいます。そこで私は最近 novoda/bintray-release に乗り換えました(READMEより)。
publish {
userOrg = 'novoda'
groupId = 'com.novoda'
artifactId = 'bintray-release'
publishVersion = '0.3.0'
desc = 'Oh hi, this is a nice description for a project, right?'
website = 'https://github.com/novoda/bintray-release'
}
公式のライブラリの方が(コードを読んでいないのでなんとも言えませんがおそらく)細かく設定できるのですが、bintray-releaseで十分な場合はこちらの方が簡潔に書けて良いと思いました。
バージョン関連
IntentBuilderは開発自体は活発ではないのですが、ビルドの設定になるほどなというテクニックが使われていたので共有します。
バージョン情報
version.properties
というファイルにバージョン情報が保存されています。
#Sun, 28 Jun 2015 19:01:31 -0700
major=0
minor=13
patch=0
ビルドファイルに以下のように書いておくと VERSION
で参照することができて良さそうでした。
ext {
VERSION = version()
}
task version << {
println version()
}
def String version() {
def versionPropsFile = file('version.properties')
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
return versionProps['major'] + "." + versionProps['minor'] + "." + versionProps['patch']
}
バージョンアップ
./gradlew bumpMajor
./gradlew bumpMinor
./gradlew bumpPatch
上のタスクを実行することでそれぞれ version.properties
の数値をインクリメントしてくれます。bumpMajor
や bumpMinor
は自分より小さい桁の数字をゼロに戻すので、たとえば 0.1.5
の状態で bumpMajor
を実行すると 1.0.0
になります。
READMEの更新
ライブラリを使う人がコピペして使えるようにUsageに以下のように書くことがあると思います。
dependencies {
compile 'se.emilsjolander:intentbuilder-api:0.13.0'
apt 'se.emilsjolander:intentbuilder-compiler:0.13.0'
}
このバージョンの更新が面倒で x.y.z
や latest-version
と書いてあるライブラリがあったりしますが、使う側からすると最新のバージョンがいくつなのか調べるのが手間になったりします。
IntentBuilderでは README.md.template
というファイルに記述して ./gradlew genReadMe
タスクを実行することで埋め込まれたバージョン情報を置き換えるということをしていました。bumpVersion => genReadMe => upload
を一連のタスクにすることで開発者の手間が減らせてライブラリを使う側にも優しくなるのでいいなと思いました。
おわりに
Androidのプロジェクトは少し特殊なので、私がライブラリを作ったときはどのようなプロジェクト構成にしたらいいのか、どうやって参照したらいいのか、どうやってjarにすればいいのかなど、色々引っかかったので知見を共有できればと思いました。
拙作のライブラリはこちらになります: rejasupotaro/kvs-schema
SharedPreferencesにアクセスするクラスを生成するライブラリで、SharedPreferencesを使っているときにどんなファイル名で保存されていたか、どんなキーが保存されていたか分からなくなるという問題の解決をモチベーションに作りました。
ちなみにこのライブラリを書き始めたときはaptについてあまりよく分かっていなかったので、JavaWriterという古い方のライブラリを使ってしまってあとで書き直すことになったり、processorの方をcompilerという名前で登録してしまい(間違いではありませんが違和感がある)、変更するのが面倒でそのままになっていたりするので、これからライブラリを作ろうと思っている人のお役に立てれば幸いです。