Shiraji's Blog

KotlinPoetを使ってみた

KotlinPoetを使ってコード生成をしたので、触りだけですが、紹介したいと思います。

なおこのエントリーはKotlinPoet v0.6.0を利用しています。

想定読者

  • KotlinPoetに興味がある人
  • JavaPoetを触ったこと・勉強したことがある人

書いていないこと

  • JavaPoetの説明

KotlinPoetについて

KotlinPoetはKotlinのコードを生成することを手助けするライブラリです。JavaPoetのKotlin版というイメージです。

以下のコードが

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val greeterClass = ClassName("", "Greeter")
val file = FileSpec.builder("", "HelloWorld")
    .addType(TypeSpec.classBuilder("Greeter")
        .primaryConstructor(FunSpec.constructorBuilder()
            .addParameter("name", String::class)
            .build())
        .addProperty(PropertySpec.builder("name", String::class)
            .initializer("name")
            .build())
        .addFunction(FunSpec.builder("greet")
            .addStatement("println(%S)", "Hello, \$name")
            .build())
        .build())
    .addFunction(FunSpec.builder("main")
        .addParameter("args", String::class, VARARG)
        .addStatement("%T(args[0]).greet()", greeterClass)
        .build())
    .build()

file.writeTo(System.out)

このコードを出力します。

1
2
3
4
5
6
7
8
9
class Greeter(val name: String) {
  fun greet() {
    println("Hello, $name")
  }
}

fun main(vararg args: String) {
  Greeter(args[0]).greet()
}

KotlinPoetの紹介はKotlinConfの動画を観ると良いです。

KotlinPoetの考え方

実は上記のコード一点、非常に面白い点があります。

1
class Greeter(val name: String)

primary constructorの生成です。KotlinPoetの以下の部分で生成しています。

1
2
3
4
5
6
7
8
val file = FileSpec.builder("", "HelloWorld")
    .addType(TypeSpec.classBuilder("Greeter")
        .primaryConstructor(FunSpec.constructorBuilder()
            .addParameter("name", String::class)
            .build())
        .addProperty(PropertySpec.builder("name", String::class)
            .initializer("name")
            .build())

valのnameをprimary constructorに入れるだけなのですが、KotlinPoetでは3回もnameと記述しています。

せっかくなので、一つずつ見ていきましょう。まずPropertyを生成する場合、以下のコードになります。

1
2
3
4
val file = FileSpec.builder("", "HelloWorld")
    .addType(TypeSpec.classBuilder("Greeter")
        .addProperty(PropertySpec.builder("name", String::class)
            .build())

以下のコードが生成されます。

1
2
3
class Greeter {
    val name: String
}

次にprimary constructorにname入れたい為、primary constructorの設定を記述します。

1
2
3
4
5
6
7
val file = FileSpec.builder("", "HelloWorld")
    .addType(TypeSpec.classBuilder("Greeter")
        .primaryConstructor(FunSpec.constructorBuilder()
            .addParameter("name", String::class)
            .build())
+       .addProperty(PropertySpec.builder("name", String::class)
+           .build())

そうするとこんなコードが生成されます。

1
2
3
class Greeter(name: String) {
    val name: String
}

最後にprimary constructorとpropertyを連結するため、初期化方法を記述します。

1
2
3
4
5
6
7
8
val file = FileSpec.builder("", "HelloWorld")
    .addType(TypeSpec.classBuilder("Greeter")
        .primaryConstructor(FunSpec.constructorBuilder()
            .addParameter("name", String::class)
            .build())
        .addProperty(PropertySpec.builder("name", String::class)
+           .initializer("name")
            .build())

これでようやく以下のコードが生成されるようになります。

1
class Greeter(val name: String)

これは、記述されたコードは全て生成する。最適化はKotlinPoetがする。というKotlinPoetの考えからきているそうです。

KotlinConfの動画でも解説されていますので確認して見てください。 https://youtu.be/_obNBSldffw?t=20m40s

若干文法が違ったり、上記のような隠れた癖がある為、JavaPoetに慣れている方は最初戸惑うことがあるかもしれませんので、生成後のコードをしっかり確認した方が良いです。

ちなみにPermissionsDispatcherはKotlinPoetのこの挙動を知らず、誤って外に出てしまったpropertyをコンストラクタに詰める為の修正をv3.0.1で入れています:joy:

その時の開発者のつぶやきです。

その他の代表的なコードの生成方法

ファイル、クラス、クラスメンバー、プライマリコンストラクター、トップレベルの関数に関しては上記サンプルコードをみてください。

それ以外のよく使いそうなコードの生成方法をメモしておきます。

コメント

kotlinpoetのコード

1
val file = FileSpec.builder("com.github.shiraji", "HelloWorld").addComment("コメント").build()

生成されるコード

1
2
// コメント
package com.github.shiraji

FileSpec.BuilderaddCommentを利用している為、ファイル上部にコメントしていますが、Builderの種類(TypeSpec.Builderなど)によりコメント位置が調整されます。

フォーマット

1
2
val message = "ふふふ"
val file = FileSpec.builder("com.github.shiraji", "HelloWorld").addComment("コメント %L", message).build()
1
2
// コメント ふふふ
package com.github.shiraji

フォーマットはJavaPoetと違い%を利用します。その他のフォーマットはこちらを参照してください。

これ以降のコードはあまりフォーマットを利用していませんが、本来はこのフォーマットを使う方が良いです。

initメソッド

1
2
3
4
5
6
7
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Greeter")
                .addInitializerBlock(CodeBlock.builder()
                        .addStatement("val i = 10")
                        .build())
                .build())
        .build()
1
2
3
4
5
6
7
package com.github.shiraji

class Greeter {
  init {
    val i = 10
  }
}

Secondary Constructor

1
2
3
4
5
6
7
8
9
10
11
12
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Greeter")
                .primaryConstructor(FunSpec.constructorBuilder()
                        .addParameter("name", String::class)
                        .build())
                .addFunction(FunSpec.constructorBuilder()
                        .callThisConstructor("name") // callSuperConstructorもあります。
                        .addParameter("name", String::class)
                        .addParameter("lastname", String::class)
                        .build())
                .build())
        .build()
1
2
3
4
5
6
7
package com.github.shiraji

import kotlin.String

class Greeter(name: String) {
  constructor(name: String, lastname: String) : this(name)
}

拡張関数

1
2
3
4
5
6
7
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Greeter")
                .addFunction(FunSpec.builder("foo")
                        .receiver(String::class)
                        .build())
                .build())
        .build()
1
2
3
4
5
6
7
8
package com.github.shiraji

import kotlin.String

class Greeter {
  fun String.foo() {
  }
}

Class修飾子(Dataクラス)

1
2
3
4
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Foo")
                .addModifiers(KModifier.DATA) // KModifier.ENUMなどもあります
                .build())
1
2
3
package com.github.shiraji

data class Foo

if/else

1
2
3
4
5
6
7
8
9
10
11
12
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Greeter")
                .addFunction(FunSpec.builder("foo")
                        .beginControlFlow("if (true)")
                        .addStatement("val i1 = 10")
                        .endControlFlow()
                        .beginControlFlow("else")
                        .addStatement("val i2 = 20")
                        .endControlFlow()
                        .build())
                .build())
        .build()
1
2
3
4
5
6
7
8
9
10
11
12
package com.github.shiraji

class Greeter {
  fun foo() {
    if (true) {
      val i1 = 10
    }
    else {
      val i2 = 20
    }
  }
}

when

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val file = FileSpec.builder("com.github.shiraji", "HelloWorld")
        .addType(TypeSpec.classBuilder("Greeter")
                .addFunction(FunSpec.builder("foo")
                        .addStatement("val i = 10")
                        .beginControlFlow("when(i)")
                        .beginControlFlow("10 ->")
                        .addStatement("println(\"foo\")")
                        .endControlFlow()
                        .beginControlFlow("20 ->")
                        .addStatement("println(\"foo222\")")
                        .endControlFlow()
                        .endControlFlow()
                        .build())
                .build())
        .build()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.github.shiraji

class Greeter {
  fun foo() {
    val i = 10
    when(i) {
      10 -> {
        println("foo")
      }
      20 -> {
        println("foo222")
      }
    }
  }
}

インデント

1
val file = FileSpec.builder("com.github.shiraji", "HelloWorld").addComment("%>コメント").build()
1
2
  // コメント
  package com.github.shiraji

アンインデントされるまでインデントされ続けます。

アンインデント

1
val file = FileSpec.builder("com.github.shiraji", "HelloWorld").addComment("%>コメント%<").build()
1
2
  // コメント
package com.github.shiraji

APIドキュメント

その他知りたければ、KotlinPoetのAPIドキュメントを確認して下さい。(v0.x系以降のドキュメントはURLが変更されるかも?)

最後に

PermissionsDispatcherのこの辺りを眺めるとKotlinPoetの実装の参考になると思います。