android-kotlinpoet

转载自https://juejin.cn/post/7113898153144745998
前言

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

下载和使用

可以通过下载并引用最新的jar包或通过Maven依赖:

Maven

复制代码

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

或通过gradle依赖:

gradle

复制代码

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

最新的开发版本可以在官网中查找。

第一个例子

用我们最熟悉的HelloWorld举例:

kotlin

复制代码

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)

会生成:

kotlin

复制代码

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,方法,属性,类型别名。

kotlin

复制代码

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

文件创建之后,可以用很多方法输出:

kotlin

复制代码

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

kotlin

复制代码

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

可以使用superclass指定父类,使用addSuperinterface指定实现的接口:

kotlin

复制代码

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

方法(Functions

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

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

还可以使用beginControlFlowendControlFlow来帮助处理换行符、大括号和缩进:

kotlin

复制代码

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

可以进一步把循环的范围改成可以配置的:

kotlin

复制代码

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, "*"),会生成:

kotlin

复制代码

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

上面例子中的方法是有方法体的。我们可以用KModifier.ABSTRACT生成没有方法体的抽象方法。当然,抽象方法必须写在抽象类或接口里。

kotlin

复制代码

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

会生成:

kotlin

复制代码

abstract class HelloWorld {
 protected abstract fun flux()
}

我们还可以用FunSpec.Builder来给方法添加其他的修饰符,比如KModifier.INLINE。除此之外,FunSpec.Builder还可以用来配置方法的参数、可变参数、注释、注解、类型变量、返回类型、接收器类型等等。

扩展方法(Extension functions)

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

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

Kotlin中的单行方法

KotlinPoet会把以return开头的方法输出成单行方法。

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

参数的默认值

如果希望给方法的参数添加默认值。例如,希望给方法的参数b添加默认值为0。

kotlin

复制代码

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

可以用ParameterSpec.builder来配置参数的defaultValue()默认值。

kotlin

复制代码

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会用换行符替换代码块中的空格。例如下面的方法:

kotlin

复制代码

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

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

kotlin

复制代码

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

这不是期望的结果also和后面的{需要在同一行,不过我们可以用·符号来声明完全不想被替换的空格。看下面的例子:

kotlin

复制代码

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

现在将产生以下结果:

kotlin

复制代码

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

构造方法(Constructors)

FunSpec也可以用于构造函数,使用FunSpec.constructorBuilder

kotlin

复制代码

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()

会生成:

kotlin

复制代码

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

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

当我们需要指定主构造方法的时候,使用primaryConstructor()

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

不过这么生成的代码太冗余了,一般Kotlin中我们会合并同名的主构造方法参数和属性。但KotlinPoet默认不这么干,需要我们使用initializer方法,告知KotlinPoet属性会被主构造方法初始化:

kotlin

复制代码

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()

现在会生成:

kotlin

复制代码

class HelloWorld(private val greeting: String)

参数(Parameters)

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

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

如果参数有注解等额外属性,那就必须用ParameterSpec.builder()这种方式。

属性(Properties)

和参数一样,属性可以使用PropertySpec.builderTypeSpec.addProperty创建:

kotlin

复制代码

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()

会生成:

kotlin

复制代码

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

如果需要给属性设定KDoc、注释或初始值,需要使用PropertySpec.builder。初始值使用initializerString.format()

kotlin

复制代码

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

会生成:

kotlin

复制代码

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

PropertySpec.Builder默认会生成val的属性,可以通过mutable()把属性设置成var

kotlin

复制代码

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

内联属性(Inline properties)

需要特别提一下KotlinPoet对内联属性的处理:

kotlin

复制代码

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

上面的代码会抛出异常:

kotlin

复制代码

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方法,会被编译器内联:

kotlin

复制代码

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

添加了getter方法之后,结果如下:

kotlin

复制代码

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

如果我们想在上面的属性中添加一个非内联的setter怎么办,我们可以:

kotlin

复制代码

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()

生成的结果在预期之中:

kotlin

复制代码

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

最后如果我们又希望使用KModifier.INLINE把setter变成inline方法,KotlinPoet可以做到生成包装好的代码:

kotlin

复制代码

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

删除getter方法或setter方法的修饰符会把这种包装再打开。

之所以KotlinPoet不允许inline直接标记属性而是标记getter/setter方法的原因是,如果KotlinPoet允许inline直接标记属性,那么每次getter/setter方法状态发生变化的时候,程序员必须手动添加/删除修饰符才能获得正确且可编译的输出。

接口(Interfaces)

KotlinPoet中使用TypeSpec.interfaceBuilder定义接口,注意定义接口方法时,必须有ABSTRACT的修饰符:

kotlin

复制代码

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

生成结果如下,注意ABSTRACT的修饰符在生成代码时被省略了:

kotlin

复制代码

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

Kotlin在1.4版本加入了fun interface语法,增加了对函数式(SAM)接口的支持。在KotlinPoet 中可以使用TypeSpec.funInterfaceBuilder()创建函数式接口:

kotlin

复制代码

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:

kotlin

复制代码

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()加到类中:

kotlin

复制代码

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()来添加枚举值:

kotlin

复制代码

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

会生成:

kotlin

复制代码

enum class Roshambo {
 ROCK,
 SCISSORS,
 PAPER
}

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

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()

会生成:

kotlin

复制代码

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

匿名内部类(Anonymous Inner Classes)

上面枚举的代码中,我们使用了TypeSpec.anonymousClassBuilder()来声明匿名内部类,也可以在代码块中用%L引用:

kotlin

复制代码

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()

会生成如下的类:

kotlin

复制代码

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()

kotlin

复制代码

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

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

kotlin

复制代码

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

AnnotationSpec.builder()可以用addMember设置注解的参数:

kotlin

复制代码

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属性:

kotlin

复制代码

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

注解的属性也可以是注解,用%L做占位符:

kotlin

复制代码

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()

会生成:

kotlin

复制代码

@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):

kotlin

复制代码

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()

会生成:

kotlin

复制代码

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

结尾

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值