Kotlin Advent Calendar 2015 20日目を担当します @laprasDrum ごとくろねこと申します。
普段は Java/Kotlin の Android アプリを書いたり、Objective-C/Swift で iOS アプリ書いたりしてます。
今回は kapt のお話を中心にします。
kapt (Kotlin Annotation Processing) とは
Java6 から導入された Pluggable Annotation Processing API (JSR 269) を Kotlin 上でも利用可能にしたものです。
実装はkotlin/libraries/tools/kotlin-annotation-processingにあります。
Pluggable Annotation Processing API 自体の解説は櫻庭さんの連載記事に分かりやすくまとまっています。
構想
Kotlin Blog にて kapt が触れられたのは2015年の5月末なので M12 が出る直前ですね。
kapt: Annotation Processing for Kotlin では、Java 以外の言語が Annotation Processor に対応する3つの案を述べています。
- JSR 269 を再実装する
- 実装コストはそこまで高くない(中の人曰く)
- Kotlin と Java が混在するプロジェクトでは役に立たない
- Java との互換性を考慮している Kotlin の思想として Kotlin しか恩恵を受けられないのはちょっと…
- Kotlin ファイルから Java ファイルを生成
- メソッドの中身まで動作保証する Java ファイルを生成するのはかなりの難易度
- Java のスタブを生成する
- Groovy の annotation processing サポート方法がそれにあたる
- スタブ生成のためのコンパイル、生成ファイルのコンパイルと手順が増えるため、ビルド時間が長くなる
- Java ファイルのアノテーション処理時に Kotlin ファイルのアノテーション処理を含める
- 通常 javac よりも先に kotlinc が動作する
- Kotlin のバイナリファイルは javac のコンパイル対象となる
- ただし javac にはアノテーション付与された Kotlin ファイルまで自動的に含ませる機構はない
- 実装は可能だし、なにより得られる恩恵は大きい
- 制約として、Kotlin ファイルからはアノテーション処理によって生成されたソースや宣言を参照することができない
Kotlin 開発チームは3つ目の選択肢に進み、僕たちは kapt の恩恵を受けられるようになりました。
が、上記の点をしっかり読まずに開発を進めているとすぐに問題に突き当たります。
昨今の Android 開発で必ずと言っていいほどお世話になる DI ツールの話です。
kapt と DI
ここから先は Dagger2 を利用した Android 開発を例とします。
Dagger2 をはじめとする JSR 330 準拠の DI ツールは javac 実行時にアノテーションを処理してファイルを生成します。
Dagger2 で生成されるコンポーネントは@component
を付与されたインタフェース名に基づいでクラス名が決定され、javac 実行後にクラス参照可能となります。
もしコンポーネントを下記のように定義した場合
@Component(modules = arrayOf(AndroidModule::class, ...))
public interface KaptamComponent {
...
コンポーネントはDaggerKaptamComponent
として参照することができます。
ビルド時の悩み
これまで Android Java のみで Android 開発していた方は生成タイミングを気にすることはあまりなかったことかと思います。
(自分もそうでした)
しかしながら Kotlin を導入するにあたって、Kotlin ファイルからコンポーネントを参照すると…
Information:Gradle tasks [:app:assembleDebug]
...
:app:compileDebugKotlin
/path/to/your/file/which/refer/to/Component.kt
Error:(12, 20) Unresolved reference: DaggerKaptamComponent
Error:Execution failed for task ':app:compileDebugKotlin'.
> Compilation error. See log for more details
Information:BUILD FAILED
Error:(12, 20) Unresolved reference: DaggerKaptamComponent
はて、もう一度エラーメッセージを確認してみましょう。
Information:Gradle tasks [:app:assembleDebug]
...
:app:compileDebugKotlin
:app:compileDebugJavaWithJavac
...
お気づきでしょうか。Java ファイルよりも Kotlin ファイルのコンパイルの方が優先されています。
繰り返しになりますが、
- Java ファイルのアノテーション処理時に Kotlin ファイルのアノテーション処理を含める
- 通常 javac よりも先に kotlinc が動作する
- Kotlin のバイナリファイルは javac のコンパイル対象となる
- ただし javac にはアノテーション付与された Kotlin ファイルまで自動的に含ませる機構はない
- 実装は可能だし、なにより得られる恩恵は大きい
- 制約として、Kotlin ファイルからはアノテーション処理によって生成されたソースや宣言を参照することができない
という体験をここで味わうことになります。
kapt.generateStubs
5月の発表から1ヶ月後、Better Annotation Processing: Supporting Stubs in kaptにてスタブが提供されるようになりました。
構想で論じた2点目の話です。
これを有効にするのがkapt.generateStubs
であり、build.gradle
に宣言することでスタブを使用できます。
kapt {
generateStubs = true
}
先に論じたとおり、スタブのためのコンパイルを含みビルド時間が長くなるため、通常はfalse
となっています。
ただし、kapt が生成するスタブは Java ファイルではなくバイナリファイルであり、メソッド本体のコード生成をスキップできることは javac の実行時間を抑えることに繋がるというメリットになります。
ともあれ、これでコンポーネント参照を Kotlin ファイルに記述しても問題なくなりました。
ちなみに
この問題、コンポーネントを参照するクラスを Java ファイルで代用すれば、generateStubs の設定に関わらずビルド順の問題を解決できます。
public class InjectorJava {
private static KaptamComponent component;
component = DaggerKaptamComponent.builder()
.androidModule(new AndroidModule(application))
.build();
}
100% Java Interoperability を謳う Kotlin ならではですね。
ちなみにちなみに
すべて僕がぶち当たったお話です。
ドキュメントはちゃんと読みましょう。
今回の検証コードはこちらにありますのでどうぞご自由にお使い下さい。
laprasdrum/kaptam
明日は AAkira さんです。お楽しみに。