前言

最近写了一个IDL转Kotlin Model Class的Android Studio插件,用到了KotlinPoet这个Square开发的开源库,KotlinPoet是用于生成.kt源文件的Kotlin和JavaAPI。在进行注释处理开发或者处理元数据文件时(例如数据库schemas、protocol、idl)KotlinPoet很有用。开发过程中,发现KotlinPoet的手册没人翻译,也没有一个系统性的教程。所以就对KotlinPoet的手册进行了翻译和收拾。

下载和运用

能够经过下载并引用最新的jar包或经过Maven依赖:

<dependency>
  <groupId>com.squareup</groupId>
  <artifactId>kotlinpoet</artifactId>
  <version>[version]</version>
</dependency>

或经过gradle依赖:

implementation("com.squareup:kotlinpoet:[version]")

最新的开发版本能够在官网中查找。

第一个比如

用咱们最了解的HelloWorld举例:

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(%P)", "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)

会生成:

class Greeter(val name: String) {
  fun greet() {
    println("""Hello, $name""")
  }
}
fun main(vararg args: String) {
  Greeter(args[0]).greet()
}

为了最大化可移植性和兼容性,如果不指定润饰符,KotlinPoet生的类、办法和变量都带有public。为简练起见,示例中省掉了润饰符。

文件(File)

KotlinPoet顶用FileSpec.builder创立文件,文件是最终代码输出的载体,能够增加一些顶级的目标例如类,objects,办法,特点,类型别号。

val file = FileSpec.builder("", "HelloWorld")
  .addComment()
  .addAnnotation()
  .addImport()
  .addProperty()
  .addType()
  .addFunction()
  .build()

文件创立之后,能够用许多办法输出:

fun writeTo(out: Appendable)
fun writeTo(directory: Path)
fun writeTo(directory: File): Unit = writeTo(directory.toPath())
fun writeTo(filer: Filer)
fun toString(): String = buildString { writeTo(this) } 

输出的时分,会按照注释、注解、包名、import、其他成员这个次序输出。

类(Type)

KotlinPoet中运用TypeSpec.classBuilder来创立类、object、接口和枚举,这里咱们只说类,其他的后面会提到。类中能够增加注释、注解、特点、办法、润饰符等候:

TypeSpec.classBuilder("Greeter")
  .addKdoc()
  .addAnnotations()
  .addProperty()
  .addFunction()
  .addModifiers()
  .build()

能够运用superclass指定父类,运用addSuperinterface指定实现的接口:

TypeSpec.classBuilder("Greeter")
  .superclass(BaseResponse::class)
  .addSuperinterface(KeepElement::class)
  .build()

办法(Functions

KotlinPoet没有给代码块结构模型,没有表达式类、语句类或语法树节点。KotlinPoet直接运用字符串作为代码块,所以方体运用带占位符(KotlinPoet中的占位符用法比较复杂所以咱们在下中讨论)的字符串构成,能够用Kotlin的多行字符串改进代码风格:

val main = FunSpec.builder("main")
  .addCode("""
    |var total = 0
    |for (i in 0 until 10) {
    |    total += i
    |}
    |""".trimMargin())
  .build()

会生成:

fun main() {
  var total = 0
  for (i in 0 until 10) {
    total += i
  }
}

还能够运用beginControlFlowendControlFlow来协助处理换行符、大括号和缩进:

val main = FunSpec.builder("main")
  .addStatement("var total = 0")
  .beginControlFlow("for (i in 0 until 10)")
  .addStatement("total += i")
  .endControlFlow()
  .build()

能够进一步把循环的范围改成能够装备的:

private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {
  return FunSpec.builder(name)
    .returns(Int::class)
    .addStatement("var result = 1")
    .beginControlFlow("for (i in $from until $to)")
    .addStatement("result = result $op i")
    .endControlFlow()
    .addStatement("return result")
    .build()
}

然后调用computeRange("multiply10to20", 10, 20, "*"),会生成:

fun multiply10to20(): kotlin.Int {
  var result = 1
  for (i in 10 until 20) {
    result = result * i
  }
  return result
}

上面比如中的办法是有办法体的。咱们能够用KModifier.ABSTRACT生成没有办法体的笼统办法。当然,笼统办法有必要写在笼统类或接口里。

val flux = FunSpec.builder("flux")
  .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addModifiers(KModifier.ABSTRACT)
  .addFunction(flux)
  .build()

会生成:

abstract class HelloWorld {
  protected abstract fun flux()
}

咱们还能够用FunSpec.Builder来给办法增加其他的润饰符,比如KModifier.INLINE。除此之外,FunSpec.Builder还能够用来装备办法的参数、可变参数、注释、注解、类型变量、返回类型、接收器类型等等。

扩展办法(Extension functions)

FunSpec.Builder指定接收器类型就能生成扩展办法。

val square = FunSpec.builder("square")
  .receiver(Int::class)
  .returns(Int::class)
  .addStatement("var s = this * this")
  .addStatement("return s")
  .build()

会生成:

fun Int.square(): Int {
  val s = this * this
  return s
}

Kotlin中的单行办法

KotlinPoet会把以return最初的办法输出成单行办法。

val abs = FunSpec.builder("abs")
  .addParameter("x", Int::class)
  .returns(Int::class)
  .addStatement("return if (x < 0) -x else x")
  .build()

会生成:

fun abs(x: Int): Int = if (x < 0) -x else x

参数的默许值

如果期望给办法的参数增加默许值。例如,期望给办法的参数b增加默许值为0。

fun add(a: Int, b: Int = 0) {
  print("a + b = ${a + b}")
}

能够用ParameterSpec.builder来装备参数的defaultValue()默许值。

FunSpec.builder("add")
  .addParameter("a", Int::class)
  .addParameter(
    ParameterSpec.builder("b", Int::class)
      .defaultValue("%L", 0)
      .build()
  )
  .addStatement("print("a + b = ${a + b}")")
  .build()

空格默许换行!

当代码行或许超越长度约束的时分,KotlinPoet会用换行符替换代码块中的空格。例如下面的办法:

val funSpec = FunSpec.builder("foo")
  .addStatement("return (100..10000).map { number -> number * number }.map { number -> number.toString() }.also { string -> println(string) }")
  .build()

会生成下面的代码,能够看出also后面就换行了:

fun foo() = (100..10000).map { number -> number * number }.map { number -> number.toString() }.also
{ string -> println(string) }

这不是期望的成果also和后面的{需求在同一行,不过咱们能够用符号来声明完全不想被替换的空格。看下面的比如:

val funSpec = FunSpec.builder("foo")
  .addStatement("return (100..10000).map{ number -> number * number }.map{ number -> number.toString() }.also{ string -> println(string) }")
  .build()

现在将发生以下成果:

fun foo() = (100..10000).map { number -> number * number }.map { number ->
  number.toString()
}.also { string -> println(string) }

结构办法(Constructors)

FunSpec也能够用于结构函数,运用FunSpec.constructorBuilder

val flux = FunSpec.constructorBuilder()
  .addParameter("greeting", String::class)
  .addStatement("this.%N = %N", "greeting", "greeting")
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addProperty("greeting", String::class, KModifier.PRIVATE)
  .addFunction(flux)
  .build()

会生成:

class HelloWorld {
  private val greeting: String
  constructor(greeting: String) {
    this.greeting = greeting
  }
}

结构办法和其他办法的生成办法是一样的。KotlinPoet会把结构办法放在其他办法的最前面。

当咱们需求指定主结构办法的时分,运用primaryConstructor()

val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .primaryConstructor(flux)
  .addProperty("greeting", String::class, KModifier.PRIVATE)
  .build()

会生成:

class HelloWorld(greeting: String) {
  private val greeting: String
  init {
    this.greeting = greeting
  }
}

不过这么生成的代码太冗余了,一般Kotlin中咱们会集并同名的主结构办法参数和特点。但KotlinPoet默许不这么干,需求咱们运用initializer办法,告知KotlinPoet特点会被主结构办法初始化:

val flux = FunSpec.constructorBuilder()
  .addParameter("greeting", String::class)
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .primaryConstructor(flux)
  .addProperty(
    PropertySpec.builder("greeting", String::class)
      .initializer("greeting")
      .addModifiers(KModifier.PRIVATE)
      .build()
  )
  .build()

现在会生成:

class HelloWorld(private val greeting: String)

参数(Parameters)

能够用ParameterSpec.builder()或者直接用FunSpec.addParameter()来声明一个参数:

val android = ParameterSpec.builder("android", String::class)
  .defaultValue(""pie"")
  .build()
val welcomeOverlords = FunSpec.builder("welcomeOverlords")
  .addParameter(android)
  .addParameter("robot", String::class)
  .build()

会生成:

fun welcomeOverlords(android: String = "pie", robot: String) {
}

如果参数有注解等额定特点,那就有必要用ParameterSpec.builder()这种办法。

特点(Properties)

和参数一样,特点能够运用PropertySpec.builderTypeSpec.addProperty创立:

val android = PropertySpec.builder("android", String::class)
  .addModifiers(KModifier.PRIVATE)
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addProperty(android)
  .addProperty("robot", String::class, KModifier.PRIVATE)
  .build()

会生成:

class HelloWorld {
  private val android: String
  private val robot: String
}

如果需求给特点设定KDoc、注释或初始值,需求运用PropertySpec.builder。初始值运用initializerString.format()

val android = PropertySpec.builder("android", String::class)
  .addModifiers(KModifier.PRIVATE)
  .initializer("%S + %L", "Oreo v.", 8.1)
  .build()

会生成:

private val android: String = "Oreo v." + 8.1

PropertySpec.Builder默许会生成val的特点,能够经过mutable()把特点设置成var

val android = PropertySpec.builder("android", String::class)
  .mutable()
  .addModifiers(KModifier.PRIVATE)
  .initializer("%S + %L", "Oreo v.", 8.1)
  .build()

内联特点(Inline properties)

需求特别提一下KotlinPoet对内联特点的处理:

val android = PropertySpec.builder("android", String::class)
  .mutable()
  .addModifiers(KModifier.INLINE)
  .build()

上面的代码会抛出反常:

java.lang.IllegalArgumentException: KotlinPoet doesn't allow setting the inline modifier on
properties. You should mark either the getter, the setter, or both inline.

这是因为被inline润饰的特点应该至少有一个getter办法,会被编译器内联:

val android = PropertySpec.builder("android", String::class)
  .mutable()
  .getter(
    FunSpec.getterBuilder()
      .addModifiers(KModifier.INLINE)
      .addStatement("return %S", "foo")
      .build()
  )
  .build()

增加了getter办法之后,成果如下:

var android: kotlin.String
  inline get() = "foo"

如果咱们想在上面的特点中增加一个非内联的setter怎么办,咱们能够:

val android = PropertySpec.builder("android", String::class)
  .mutable()
  .getter(
    FunSpec.getterBuilder()
      .addModifiers(KModifier.INLINE)
      .addStatement("return %S", "foo")
      .build()
  )
  .setter(
    FunSpec.setterBuilder()
      .addParameter("value", String::class)
      .build()
  )
  .build()

生成的成果在预期之中:

var android: kotlin.String
  inline get() = "foo"
  set(`value`) {
  }

最终如果咱们又期望运用KModifier.INLINE把setter变成inline办法,KotlinPoet能够做到生成包装好的代码:

inline var android: kotlin.String
  get() = "foo"
  set(`value`) {
  }

删去getter办法或setter办法的润饰符会把这种包装再翻开。

之所以KotlinPoet不允许inline直接符号特点而是符号getter/setter办法的原因是,如果KotlinPoet允许inline直接符号特点,那么每次getter/setter办法状况发生变化的时分,程序员有必要手动增加/删去润饰符才能取得正确且可编译的输出。

接口(Interfaces)

KotlinPoet中运用TypeSpec.interfaceBuilder定义接口,留意定义接口办法时,有必要有ABSTRACT的润饰符:

val helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
  .addProperty("buzz", String::class)
  .addFunction(
    FunSpec.builder("beep")
      .addModifiers(KModifier.ABSTRACT)
      .build()
  )
  .build()

生成成果如下,留意ABSTRACT的润饰符在生成代码时被省掉了:

interface HelloWorld {
  val buzz: String
  fun beep()
}

Kotlin在1.4版本加入了fun interface语法,增加了对函数式(SAM)接口的支撑。在KotlinPoet 中能够运用TypeSpec.funInterfaceBuilder()创立函数式接口:

val helloWorld = TypeSpec.funInterfaceBuilder("HelloWorld")
  .addFunction(
    FunSpec.builder("beep")
      .addModifiers(KModifier.ABSTRACT)
      .build()
  )
  .build()
// Generates...
fun interface HelloWorld {
  fun beep()
}

目标声明(Object)

KotlinPoet中运用TypeSpec.objectBuilder声明Object:

val helloWorld = TypeSpec.objectBuilder("HelloWorld")
  .addProperty(
    PropertySpec.builder("buzz", String::class)
      .initializer("%S", "buzz")
      .build()
  )
  .addFunction(
    FunSpec.builder("beep")
      .addStatement("println(%S)", "Beep!")
      .build()
  )
  .build()

KotlinPoet相同也支撑用TypeSpec.companionObjectBuilder声明伴生目标,并用addType()加到类中:

val companion = TypeSpec.companionObjectBuilder()
  .addProperty(
    PropertySpec.builder("buzz", String::class)
      .initializer("%S", "buzz")
      .build()
  )
  .addFunction(
    FunSpec.builder("beep")
      .addStatement("println(%S)", "Beep!")
      .build()
  )
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addType(companion)
  .build()

伴生目标也能够指定名称,如果不指定的话会运用默许名称Companion

枚举(Enums)

KotlinPoet运用于TypeSpec.enumBuilder创立枚举类型,并运用addEnumConstant()来增加枚举值:

val helloWorld = TypeSpec.enumBuilder("Roshambo")
  .addEnumConstant("ROCK")
  .addEnumConstant("SCISSORS")
  .addEnumConstant("PAPER")
  .build()

会生成:

enum class Roshambo {
  ROCK,
  SCISSORS,
  PAPER
}

KotlinPoet支撑Kotlin中花哨的枚举,支撑枚举值重写办法或调用超类的结构函数。示例:

val helloWorld = TypeSpec.enumBuilder("Roshambo")
  .primaryConstructor(
    FunSpec.constructorBuilder()
      .addParameter("handsign", String::class)
      .build()
  )
  .addEnumConstant(
    "ROCK", TypeSpec.anonymousClassBuilder()
      .addSuperclassConstructorParameter("%S", "fist")
      .addFunction(
        FunSpec.builder("toString")
          .addModifiers(KModifier.OVERRIDE)
          .addStatement("return %S", "avalanche!")
          .returns(String::class)
          .build()
      )
      .build()
  )
  .addEnumConstant(
    "SCISSORS", TypeSpec.anonymousClassBuilder()
      .addSuperclassConstructorParameter("%S", "peace")
      .build()
  )
  .addEnumConstant(
    "PAPER", TypeSpec.anonymousClassBuilder()
      .addSuperclassConstructorParameter("%S", "flat")
      .build()
  )
  .addProperty(
    PropertySpec.builder("handsign", String::class, KModifier.PRIVATE)
      .initializer("handsign")
      .build()
  )
  .build()

会生成:

enum class Roshambo(private val handsign: String) {
  ROCK("fist") {
    override fun toString(): String = "avalanche!"
  },
  SCISSORS("peace"),
  PAPER("flat");
}

匿名内部类(Anonymous Inner Classes)

上面枚举的代码中,咱们运用了TypeSpec.anonymousClassBuilder()来声明匿名内部类,也能够在代码块顶用%L引用:

val comparator = TypeSpec.anonymousClassBuilder()
  .addSuperinterface(Comparator::class.parameterizedBy(String::class))
  .addFunction(
    FunSpec.builder("compare")
      .addModifiers(KModifier.OVERRIDE)
      .addParameter("a", String::class)
      .addParameter("b", String::class)
      .returns(Int::class)
      .addStatement("return %N.length - %N.length", "a", "b")
      .build()
  )
  .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
  .addFunction(
    FunSpec.builder("sortByLength")
      .addParameter("strings", List::class.parameterizedBy(String::class))
      .addStatement("%N.sortedWith(%L)", "strings", comparator)
      .build()
  )
  .build()

会生成如下的类:

class HelloWorld {
  fun sortByLength(strings: List<String>) {
    strings.sortedWith(object : Comparator<String> {
      override fun compare(a: String, b: String): Int = a.length - b.length
    })
  }
}

能够运用TypeSpec.Builder.addSuperclassConstructorParameter()办法传递超类结构函数的参数。

注解(Annotations)

不带参数的,简略的注解能够直接用addAnnotation()

val test = FunSpec.builder("test string equality")
  .addAnnotation(Test::class)
  .addStatement("assertThat(%1S).isEqualTo(%1S)", "foo")
  .build()

在办法上生成了@Test注解:

@Test
fun `test string equality`() {
  assertThat("foo").isEqualTo("foo")
}

AnnotationSpec.builder()能够用addMember设置注解的参数:

val logRecord = FunSpec.builder("recordEvent")
  .addModifiers(KModifier.ABSTRACT)
  .addAnnotation(
    AnnotationSpec.builder(Headers::class)
      .addMember("accept = %S", "application/json; charset=utf-8")
      .addMember("userAgent = %S", "Square Cash")
      .build()
  )
  .addParameter("logRecord", LogRecord::class)
  .returns(LogReceipt::class)
  .build()

生成的注解会带着acceptuserAgent特点:

@Headers(
  accept = "application/json; charset=utf-8",
  userAgent = "Square Cash"
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt

注解的特点也能够是注解,用%L做占位符:

val headerList = ClassName("", "HeaderList")
val header = ClassName("", "Header")
val logRecord = FunSpec.builder("recordEvent")
  .addModifiers(KModifier.ABSTRACT)
  .addAnnotation(
    AnnotationSpec.builder(headerList)
      .addMember(
        "[\n⇥%L,\n%L⇤\n]",
        AnnotationSpec.builder(header)
          .addMember("name = %S", "Accept")
          .addMember("value = %S", "application/json; charset=utf-8")
          .build(),
        AnnotationSpec.builder(header)
          .addMember("name = %S", "User-Agent")
          .addMember("value = %S", "Square Cash")
          .build()
      )
      .build()
  )
  .addParameter("logRecord", logRecordName)
  .returns(logReceipt)
  .build()

会生成:

@HeaderList(
  [
    Header(name = "Accept", value = "application/json; charset=utf-8"),
    Header(name = "User-Agent", value = "Square Cash")
  ]
)
abstract fun recordEvent(logRecord: LogRecord): LogReceipt

KotlinPoet支撑注解运用处目标(Annotation use-site targets):

val utils = FileSpec.builder("com.example", "Utils")
  .addAnnotation(
    AnnotationSpec.builder(JvmName::class)
      .useSiteTarget(UseSiteTarget.FILE)
      .build()
  )
  .addFunction(
    FunSpec.builder("abs")
      .receiver(Int::class)
      .returns(Int::class)
      .addStatement("return if (this < 0) -this else this")
      .build()
  )
  .build()

会生成:

@file:JvmName
package com.example
import kotlin.Int
import kotlin.jvm.JvmName
fun Int.abs(): Int = if (this < 0) -this else this

结束

下部分咱们会讨论占位符的用法,和剩下的一小部分KotinPoet手册的内容。