JavaScriptを有効にしてください

kaptでHelloWorld

 ·  ☕ 8 分で読めます  ·  ✍️ saiki

おはようございます。毎度おなじみsaikiです。

タイトル通り、kaptでHelloWorld的な事をしていこうと思います。

サンプルリポジトリ:https://github.com/sasasaiki/my-kapt-sample/commits/master

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🏃‍♂️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を使うのが良さそうです。

Medium.com で表示

ビルド

ここまでやってビルドするとパス/ファイルにコードが生成されます。
今回生成されたコードは下記になります。
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

Medium.com で表示

Medium.com で表示

Medium.com で表示

https://techblog.yahoo.co.jp/advent-calendar-2016/transform_api/

ではまた。

共有

saiki
著者
saiki
Android App Developper