KotlinPoet详细使用指南(上)

前言

最近写了一个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、接口和枚举,这里我们只说类,其他的后面会提到。类中可以添加注释、注解、属性、方法、修饰符等待:

扫描二维码关注公众号,回复: 14333833 查看本文章
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手册的内容。

猜你喜欢

转载自juejin.im/post/7113898153144745998
今日推荐