おはようございます。毎度おなじみsaikiです。
タイトル通り、kaptでHelloWorld的な事をしていこうと思います。
サンプルリポジトリ:https://github.com/sasasaiki/my-kapt-sample/commits/master
目次
<li>
<a href="#i-8">まとめ</a>
</li>
kaptとは?#
kotlin-annotation-processing tools の略(多分)でjavaのPluggable Annotation Processing API をkotlinでも使えるようにするためのpluginです。
要するとkotlinでアノテーション(@Hogeみたいなやつ)を使ってコードを生成するための仕組みです。
こいつを使うことで、コンパイル時にアノテーションをつけたクラスや関数の情報を元にコードを生成することができます。
AndroidだとDagger2やLifeCycleに使われています。
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) func hoge(){}
こんな感じで@OnLifecycleEventをつけるとそれをライフサイクルに従って実行するようなコードが生成されるわけです。
いままで@ってそういうもんなんだろうなあという程度の認識で深く考えていませんでしたがこういうことだったんですね。
(ただし@がついているからといってannotationProcessingとは限らないです)
使ってみる#
ということで使ってみましょう。
AndroidStudioでやります。
プロジェクトを作る#
普通にプロジェクトを作りましょう。
Kotlinであれば他はなんでもいいです。
モジュールを作る#
今回実装するモジュールを作ります。
Kotlinで書きますがjavaモジュールとして作ります。
アノテーションを実装するannotationとコード生成部分を実装するgeneraterを作成します。
一般的のライブラリもメインのモジュールとなんたら-compilerみたいに別れてることが多いようです。
私もなんたら-compilerみたいな名前にすればよかったなあと思いつつ本筋とは関係ないのでそのまま進みます。
build.gradleを書く(鬼門)#
なんか知らないけどものすごく苦労しました。
AndroidStudioが急に自動生成してくれたりしますがコンパイル通らないことが多々あるので気を付けましょう。
モジュール2つとappのbuild.gradleをいじります。
まずはapp/build.gradle
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt'//ここを追加 sourceSets { main { java { srcDir "${buildDir.absolutePath}/tmp/kapt/main/kotlinGenerated/"//コマンドでビルド実行するときに必要?忘れた } } } android { compileSdkVersion 27 defaultConfig { applicationId "app.saiki.mykaptsample" minSdkVersion 23 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //下記2行を追加 kapt project(':generator') implementation project(':annotation') }
kotlin-kaptと二つのモジュールへの依存を追加します。
generatorの方はkaptであることに注意しましょう。
コメントにもありますがsrcDirはなんかコマンドで実行するときに必要みたいな記述をどこかで見た気がして追加したのですが忘れました。
次はgenerator/build.gradle
//plugins { // id 'org.jetbrains.kotlin.jvm' version '1.2.61' //} apply plugin: 'kotlin' apply plugin: 'kotlin-kapt'//これと dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" //ここから implementation 'com.squareup:kotlinpoet:0.7.0' implementation "com.google.auto.service:auto-service:1.0-rc4" kapt "com.google.auto.service:auto-service:1.0-rc4" implementation project(':annotation') //ここまで } sourceCompatibility = "7" targetCompatibility = "7" repositories { mavenCentral() } compileKotlin { kotlinOptions { jvmTarget = "1.8" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" } }
先ほどと同じくkotlin-kaptをapplyします。
いくつか必要なライブラリがあるので追加します。
まずkotlinpoet。これはクラスを生成するときにあると便利。
次にauto-service。これはaptを使う際に必要なフォルダをいい感じに勝手にやってくれるそうです。META-INF/servicesとかそこらへんを。あんまり詳しくは調べてないですがとりあえず必要なので入れましょう。kaptとimplementation両方必要です。
最後にannotaionも追加します。コード生成時にアノテーションを読む必要があるからですね。
コメントアウトされている冒頭三行はAndroidStudioが勝手に入れてくれたのに存在しているとコンパイルが通らなかったのでコメントアウトした残骸です。
annotation/build.gradle
//plugins { // id 'org.jetbrains.kotlin.jvm' version '1.2.61' //} apply plugin: 'kotlin' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } sourceCompatibility = "7" targetCompatibility = "7" repositories { mavenCentral() } compileKotlin { kotlinOptions { jvmTarget = "1.8" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" } }
特筆することはありませんが一応貼っておきます。
最後にルートにある/setting.gradleにmoduleを追記しましょう。
(自動で入れてくれたような気もしますが)
include ':app', ':generator', ':annotation'
一旦ビルドしてみて通れば多分OK。
ここまで終われば全部終わったようなものです。
嘘です。
アノテーションを定義#
annotationモジュールに適当なファイルを作ってアノテーションを定義します。
今回はapp/saiki/annotation/Greeting.ktとします。
package app.saiki.annotation @Target(AnnotationTarget.CLASS) annotation class Greeting @Target(AnnotationTarget.FUNCTION) annotation class GreetingForFunc
簡単ですね。
@Targetを使うことでクラス用なのかファンクション用なのか指定することができます。
アノテーションをつける#
先ほど定義したアノテーションを使ってみましょう。
今回はmainActivityにつけます。
@Greeting class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
これだけ。
processer書く#
さあ本日のメインコンテンツprocesserを書いていきます。
1ファイルなのでとりあえず全部はります。
package app.saiki.generator import app.saiki.annotation.Greeting import com.google.auto.common.BasicAnnotationProcessor import com.google.auto.service.AutoService import com.google.common.collect.SetMultimap import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.TypeSpec import java.io.File import javax.annotation.processing.Processor import javax.lang.model.SourceVersion import javax.lang.model.element.Element import javax.lang.model.element.ElementKind @AutoService(Processor::class)//auto-service使うのに必要なので忘れずに class MyProcessor : BasicAnnotationProcessor() {//AbstractProcessorもしくはBasicAnnotationProcessorを継承する companion object { private const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"//こういうもんらしい } override fun getSupportedSourceVersion() = SourceVersion.latestSupported()!!//コンパイラのサポートバージョンを指定 override fun initSteps(): MutableIterable<ProcessingStep> { val outputDirectory = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] ?.replace("kaptKotlin", "kapt")//ここでkaptKotlinをkaptに変えないと生成後のclassが読めない ?.let { File(it) } ?: throw IllegalArgumentException("No output directory!") //ここでStepたちを渡すと実行される return mutableListOf(MyProcessingStep(outputDir = outputDirectory)) } } class MyProcessingStep(private val outputDir: File) : BasicAnnotationProcessor.ProcessingStep { override fun annotations() = mutableSetOf(Greeting::class.java,Greeting::class.java)//どのアノテーションを処理するか羅列 override fun process(elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>?): MutableSet<Element> { elementsByAnnotation ?: return mutableSetOf() try { for (annotatedElement in elementsByAnnotation[Greeting::class.java]) { if (annotatedElement.kind !== ElementKind.CLASS) {//今回はClassしかこないが念のためチェック throw Exception("@${Greeting::class.java.simpleName} can annotate class type.") } // fieldにつけると$が付いてくることがあるらしいのであればとる val annotatedClassName = annotatedElement.simpleName.toString().trimDollarIfNeeded() //func生成 val generatingFunc = FunSpec .builder("greet") .addStatement("return \"Hello $annotatedClassName !!\"") .build() //class生成 val generatingClass = TypeSpec .classBuilder("${annotatedClassName}_Greeter") .addFunction(generatingFunc) .build() //書き込み FileSpec.builder("app.saiki.generated", generatingClass.name!!) .addType(generatingClass) .build() .writeTo(outputDir) } } catch (e: Exception) { throw e } // ここで何かしらをreturnすると次のステップでごにょごにょできるらしい? return mutableSetOf() } // 名前に含まれる$をとる private fun String.trimDollarIfNeeded(): String { val index = indexOf("$") return if (index == -1) this else substring(0, index) } }
だいたいコメントに書いたんで見てもらえればと思います。
AbstractProcessorもしくはBasicAnnotationProcessorを継承すると書いてありますがBasicAnnotationProcessorの方がシンプルでいいよ!みたいなことが下記参考ページに書いてありましたのでBasicAnnotationProcessorを使うのが良さそうです。
ビルド#
ここまでやってビルドするとパス/ファイルにコードが生成されます。
今回生成されたコードは下記になります。
app.saiki.generated.MainActivity_Greeter
package app.saiki.generated class MainActivity_Greeter { fun greet() = "Hello MainActivity !!" }
シンプル。
使ってみる#
最後に使ってみましょう。
すでにコード生成済みなので補完も普通に効きます。
逆に言うとコードが生成されていないと補完もコンパイルもうまくいかないのでご注意ください。
こんな感じで書いて実行すると
package app.saiki.mykaptsample import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Log import app.saiki.annotation.Greeting import app.saiki.generated.MainActivity_Greeter @Greeting class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d("kaptで",MainActivity_Greeter().greet()) } }
こんな感じで無事ログが出力されました。
まとめ#
ということでkaptでハローワールドしてみました。
より良くするなら、ビルドしてコードを生成する前でもコンパイルできるような仕組みや、そもそも呼び出しのコードを書かずともアノテーションだけで動作するようにする等といったことをすると素敵ですね。
今度やって見たいと思います。
サンプルリポジトリと参考URLです。
https://github.com/sasasaiki/my-kapt-sample/commits/master
https://techblog.yahoo.co.jp/advent-calendar-2016/transform_api/
ではまた。