KotlinPoet を使って Android プロジェクトでコードジェネレーションする

やたらはまって大変だったのに、できあがってみると差分は大したことをしてないように見える仕事

参考

PermissionsDispatcher が KotlinPoet に対応した差分がとてもとてもとても参考になります。特にこの PR です。

他には Java のコードジェネレーションのわかりやすいブログがあって、そこのサンプルコードを Kotlin にしたものもあるのですが

これは Android プロジェクトじゃないので、そのままではうまくいきません。

サンプル Pull Request

そこで、作っただけの Android Studio のプロジェクトに KotlinPoet を導入するリポジトリと PR を作ってみました。

自分がやったやり方

ちゃんとできてないこといっぱいあると思うんですけど、とりあえず生成されるところまでやったことをコミットごとに書きます。

Android Studio でモジュールを足す

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/898902a01dfe5c4b1121c990ad6b9662ef8d99ba

ここは Java と同じです。アノテーションのクラスが入ったモジュールと、それを処理するアノテーションプロセッサのモジュールの2つを作ります。

Android Studio の File メニューから [New] — [New Module..] と辿り Java Library を選びます。

ほんとは “Kotlin Library” がいいんだけど

モジュールの名前はなんでもよく、ここでは annotationprocessor とつけました。

モジュールって普段ほとんど作らないから、あってるのか不安になります

それぞれのモジュールに build.gradle ができていますので、これを Kotlin 用に変更します。

annotation

apply plugin: 'kotlin'

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

processor

apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'

dependencies {
implementation project(':annotation')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'com.google.auto:auto-common:0.8'
implementation "com.google.auto.service:auto-service:1.0-rc3"
kapt "com.google.auto.service:auto-service:1.0-rc3"
implementation 'com.squareup:kotlinpoet:0.5.0'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

僕が JavaPoet 時代に使っていた Auto というライブラリが入っていますが、当然なくても大丈夫です。

肝心の app のビルドの際にコードジェネレーションに動いてもらいたいので、 app の build.gradle も編集します。

app

kapt がいろいろやってくれる

アノテーションクラスを作る

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/20976f6575426ae08d78435f2b2319903384ebc7

@Oogatta というご機嫌なアノテーションを作ってみることにします。

annotation モジュールの中にクラスを作ります。

annotation/src/main/java/com/oogatta/annotation/Oogatta.kt

package com.oogatta.annotation

@Target(AnnotationTarget.CLASS)
annotation class Oogatta

アノテートする対象はクラスなので AnnocationTarget.CLASS にしています。早速実際にアノテートしてみます。

app/src/main/java/com/oogatta/androidkotlinpoet/MainActivity.kt

package com.oogatta.androidkotlinpoet

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.oogatta.annotation.Oogatta

@Oogatta
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

ご機嫌です。うまく設定できていれば、この時点ですでに Android Studio に認識されていて、 Oogatta を ⌘ クリックしてジャンプできるはずです。


アノテーションプロセッサを作る

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/6b3d4f687eaf3042b8524e1d1e9333de32c32569

とりあえず、何かをアノテートしたらとにかく OogattaHelper というクラスを生成する、という乱暴なプロセッサで動作を確認します。

実際にプロセッサを動かすのに必要な META-INF/services/javax.annotation.processing.Processor は kapt が勝手に作ってくれます。(実際に processor/build/tmp/kapt3/classes/main/META-INF/services/javax.annotation.processing.Processor ここに出力されていました)

ここまででビルドしてみると

app/build/generated/source/kaptKotlin/debug/com/oogatta/helper/OogattaHelper.kt

が出力されます。

出力されました

注意

Project の View が Android だと生成されたファイルは見えないよ

ここ大事

おかしいときは clean

あるいはプロセッサを変更したら

$ ./gradlew clean cleanBuildCache && ./gradlew --stop

する、のほうが、何も考えずに毎回うまくいくのでいいかも。


生成されたファイルが Android Studio に認識されるようにする

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/0f10bf925603b40ad9d443be4a8eb9faf1dfe254

生成されたはいいんですが、⌘-O に出てきません!赤いです!

Crimson Red

なのでプロセッサを変更します。

出力先を大胆に変更

kapt が出力する Kotlin のディレクトリを Android Studio が認識していないので、 PermissionsDispatcher の issue や差分を参考に、出力先を強引に変更してしまいます。

何にはまったかって、自分はこれに一番はまりました……。

OogattaHelper!

出力先を変更できれば、生成されたクラスも ⌘-O の候補に現れるようになります。


さすがに体裁を整える

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/000784828fe12d909dd2117700cddad757479140

https://github.com/oogatta/AndroidKotlinPoet/pull/1/commits/4c77984e1fd643de853c236aeba707a48625573f

プロセッサはあのままだとさすがにひどいので、アノテートされたクラスがちゃんとクラスかどうか確認したり、

override fun process(elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>?): MutableSet<Element> {
elementsByAnnotation ?: return mutableSetOf()

try {
for (annotatedElement in elementsByAnnotation[Oogatta::class.java]) {
if (annotatedElement.kind !== ElementKind.CLASS) {
throw Exception("@${Oogatta::class.java.simpleName} can annotate class type.")
}

アノテートしたクラスに基づいて出力するクラス名を変えたりするといったよくやるものをやっておきます。

val annotatedClassName = annotatedElement.simpleName.toString().trimDollarIfNeeded()

val klass = TypeSpec
.classBuilder("${annotatedClassName}Helper")
.build()

trimDollarIfNeeded の部分はまたしても僕が二番目にはまったやつで……。

この例ではアノテートの対象がクラスなので不要(だと思う)なんですが、プロパティとかメソッドとかに使い始めると途端に simpleName に $annotations とかついてきて、これが大変でした。

大胆さが必要

これも、単純に文字列操作で取っていいんだって思ったのは PermissionsDispatcher の差分をのぞいたからです。ありがとうございます……。 IfNeeded ……。

望むところに望む名前で出力されました!

完成です!