前言
最近写了一个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
}
}
还可以使用beginControlFlow
和endControlFlow
来帮助处理换行符、大括号和缩进:
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.builder
或TypeSpec.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
。初始值使用initializer
和String.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()
生成的注解会带着accept
和userAgent
属性:
@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手册的内容。