android-kotlinpoet下

转载自:https://article.juejin.cn/post/7124301561454411807

前言

上一篇文章里我们基本上讲明白了KotlinPoet是如何生成.kt源文件的。KotlinPoet给Kotlin中的每个实体都创建了Model,文件-FileSpec、类、接口和对象-TypeSpec、类型别名-TypeAliasSpec、属性-PropertySpec、方法和构造方法-FunSpec、参数-ParameterSpec和注解-AnnotationSpec

但是KotlinPoet并没有给方法体提供Model。而是直接使用字符串代替代码块,于是字符串中的占位符逻辑和应用就是很重要的一环。

代码和控制流

用%S占位字符串

在我们构造代码块的时候,可以用%S作为一个字符串的占位符:

kotlin

复制代码

fun main(args: Array<String>) {
 val helloWorld = TypeSpec.classBuilder("HelloWorld")
 .addFunction(whatsMyNameYo("slimShady"))
 .addFunction(whatsMyNameYo("eminem"))
 .addFunction(whatsMyNameYo("marshallMathers"))
 .build()
 val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld")
 .addType(helloWorld)
 .build()
 kotlinFile.writeTo(System.out)
}
private fun whatsMyNameYo(name: String): FunSpec {
 return FunSpec.builder(name)
 .returns(String::class)
 .addStatement("return %S", name)
 .build()
}

会生成(如果return语句就是方法体的第一行,就会写成kotlin中一行方法的形式):

kotlin

复制代码

class HelloWorld {
 fun slimShady(): String = "slimShady"
 fun eminem(): String = "eminem"
 fun marshallMathers(): String = "marshallMathers"
}

用%P占位字符串模板

占位符%S处理转义字符的时候,会进行变化来让转义字符表达字符本来的意思。例如美元符号$,在Kotlin的字符串中被用来当作串模版使用,如果在Kotlin字符串中需要使用$作为字符,就需要写成${'$'},占位符%S会自动进行这种处理:

ini

复制代码

val stringWithADollar = "Your total is " + "$" + "50"
val funSpec = FunSpec.builder("printTotal")
 .returns(String::class)
 .addStatement("return %S", stringWithADollar)
 .build()

会生成:

kotlin

复制代码

fun printTotal(): String = "Your total is ${'$'}50"
`

如果我们不需要这种处理,需要使用`%P`,`%P`不会处理符号的转义:

ini

复制代码

```kotlin
val amount = 50
val stringWithADollar = "Your total is " + "$" + "amount"
val funSpec = FunSpec.builder("printTotal")
 .returns(String::class)
 .addStatement("return %P", stringWithADollar)
 .build()

会生成:

kotlin

复制代码

fun printTotal(): String = "Your total is $amount"

同时%P还可以当作代码块CodeBlock(此处的CodeBlockKotlinPoet中的类)的占位符:

less

复制代码

val file = FileSpec.builder("com.example", "Digits")
 .addFunction(
 FunSpec.builder("print")
 .addParameter("digits", IntArray::class)
 .addStatement("println(%P)", buildCodeBlock {
 val contentToString = MemberName("kotlin.collections", "contentToString")
 add("These are the digits: ${digits.%M()}", contentToString)
 })
 .build()
 )
 .build()
println(file)

会生成:

python

复制代码

package com.example
import kotlin.IntArray
import kotlin.collections.contentToString
fun print(digits: IntArray) {
 println("""These are the digits: ${digits.contentToString()}""")
}

用 %T占位类

可以用%T来进行类型引用,KotlinPoet可以为被%T引用的类自动生成import 语句:

scss

复制代码

val today = FunSpec.builder("today")
 .returns(Date::class)
 .addStatement("return %T()", Date::class)
 .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
 .addFunction(today)
 .build()
val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld")
 .addType(helloWorld)
 .build()
kotlinFile.writeTo(System.out)

会生成以下.kt文件,并包含必要的import

kotlin

复制代码

package com.example.helloworld
import java.util.Date
class HelloWorld {
 fun today(): Date = Date()
}

上面我们通过传递Date::class引用了一个在我们生成代码时就可用的类。同样我们可以通过ClassName引用一个生成代码时不存在的类:

ini

复制代码

val hoverboard = ClassName("com.mattel", "Hoverboard")
val tomorrow = FunSpec.builder("tomorrow")
 .returns(hoverboard)
 .addStatement("return %T()", hoverboard)
 .build()

KotlinPoet会导入那个尚不存在的类并生成:

kotlin

复制代码

package com.example.helloworld
import com.mattel.Hoverboard
class HelloWorld {
 fun tomorrow(): Hoverboard = Hoverboard()
}

ClassNameKotlinPoet中经常会用到的很重要的类,可以识别任何被声明的类。ClassName还能识别数组类型、参数化类型、通配符类型、lambda 类型和类型变量。对每一种类型,KotlinPoet中都有用于构建的类:

ini

复制代码

import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
val hoverboard = ClassName("com.mattel", "Hoverboard")
val list = ClassName("kotlin.collections", "List")
val arrayList = ClassName("kotlin.collections", "ArrayList")
val listOfHoverboards = list.parameterizedBy(hoverboard)
val arrayListOfHoverboards = arrayList.parameterizedBy(hoverboard)
val thing = ClassName("com.misc", "Thing")
val array = ClassName("kotlin", "Array")
val producerArrayOfThings = array.parameterizedBy(WildcardTypeName.producerOf(thing))
val beyond = FunSpec.builder("beyond")
 .returns(listOfHoverboards)
 .addStatement("val result = %T()", arrayListOfHoverboards)
 .addStatement("result += %T()", hoverboard)
 .addStatement("result += %T()", hoverboard)
 .addStatement("result += %T()", hoverboard)
 .addStatement("return result")
 .build()
val printThings = FunSpec.builder("printThings")
 .addParameter("things", producerArrayOfThings)
 .addStatement("println(things)")
 .build()

KotlinPoet 会分解每种类型并在可能的情况下导入其组件:

kotlin

复制代码

package com.example.helloworld
import com.mattel.Hoverboard
import com.misc.Thing
import kotlin.Array
import kotlin.collections.ArrayList
import kotlin.collections.List
class HelloWorld {
 fun beyond(): List<Hoverboard> {
 val result = ArrayList<Hoverboard>()
 result += Hoverboard()
 result += Hoverboard()
 result += Hoverboard()
 return result
 }
 fun printThings(things: Array<out Thing>) {
 println(things)
 }
}
可空类型

KotlinPoet支持可为空的类型。使用copy(``nullable = true``)方法可把TypeName转化成可空的类型(注意可空是配置在类型上的,而不是参数/属性上):

css

复制代码

val java = PropertySpec.builder("java", String::class.asTypeName().copy(nullable = true))
 .mutable()
 .addModifiers(KModifier.PRIVATE)
 .initializer("null")
 .build()
val helloWorld = TypeSpec.classBuilder("HelloWorld")
 .addProperty(java)
 .addProperty("kotlin", String::class, KModifier.PRIVATE)
 .build()

会生成:

kotlin

复制代码

class HelloWorld {
 private var java: String? = null
 private val kotlin: String
}

用%M占位参数

与类型一样,KotlinPoet 也为成员(函数和属性)提供了一个特殊的占位符%M,如果代码中需要访问顶级成员和在对象内部声明的成员时,就需要使用%M。用%M引用成员时,需要一个MemberName作为占位符的参数,KotlinPoet 会自动处理import

ini

复制代码

val createTaco = MemberName("com.squareup.tacos", "createTaco")
val isVegan = MemberName("com.squareup.tacos", "isVegan")
val file = FileSpec.builder("com.squareup.example", "TacoTest")
 .addFunction(
 FunSpec.builder("main")
 .addStatement("val taco = %M()", createTaco)
 .addStatement("println(taco.%M)", isVegan)
 .build()
 )
 .build()
println(file)

上面的代码会生成:

kotlin

复制代码

package com.squareup.example
import com.squareup.tacos.createTaco
import com.squareup.tacos.isVegan
fun main() {
 val taco = createTaco()
 println(taco.isVegan)
}

%M也可以用来引用扩展函数和扩展属性。不过需要确保导入的成员不会发生简单名称(不包括namespace的名称)冲突,否则导入将失败并且代码生成器输出将无法通过编译。不过KotlinPoet提供了FileSpec.addAliasedImport()用于为冲突创建别名导入:

ini

复制代码

val createTaco = MemberName("com.squareup.tacos", "createTaco")
val createCake = MemberName("com.squareup.cakes", "createCake")
val isTacoVegan = MemberName("com.squareup.tacos", "isVegan")
val isCakeVegan = MemberName("com.squareup.cakes", "isVegan")
val file = FileSpec.builder("com.squareup.example", "Test")
 .addAliasedImport(isTacoVegan, "isTacoVegan")
 .addAliasedImport(isCakeVegan, "isCakeVegan")
 .addFunction(
 FunSpec.builder("main")
 .addStatement("val taco = %M()", createTaco)
 .addStatement("val cake = %M()", createCake)
 .addStatement("println(taco.%M)", isTacoVegan)
 .addStatement("println(cake.%M)", isCakeVegan)
 .build()
 )
 .build()
println(file)

KotlinPoet 会给2个同名的isVegan成员生成别名导入:

kotlin

复制代码

package com.squareup.example
import com.squareup.cakes.createCake
import com.squareup.tacos.createTaco
import com.squareup.cakes.isVegan as isCakeVegan
import com.squareup.tacos.isVegan as isTacoVegan
fun main() {
 val taco = createTaco()
 val cake = createCake()
 println(taco.isTacoVegan)
 println(cake.isCakeVegan)
}
参数名称和运算符

MemberName还支持运算符(或运算符重载),可以使用MemberName(String, KOperator)MemberName(ClassName, KOperator)来导入和引用运算符。

less

复制代码

val taco = ClassName("com.squareup.tacos", "Taco")
val meat = ClassName("com.squareup.tacos.ingredient", "Meat")
val iterator = MemberName("com.squareup.tacos.internal", KOperator.ITERATOR)
val minusAssign = MemberName("com.squareup.tacos.internal", KOperator.MINUS_ASSIGN)
val file = FileSpec.builder("com.example", "Test")
 .addFunction(
 FunSpec.builder("makeTacoHealthy")
 .addParameter("taco", taco)
 .beginControlFlow("for (ingredient %M taco)", iterator)
 .addStatement("if (ingredient is %T) taco %M ingredient", meat, minusAssign)
 .endControlFlow()
 .addStatement("return taco")
 .build()
 )
 .build()
println(file)

KotlinPoet 将导入并使用重载之后的运算符:

kotlin

复制代码

package com.example
import com.squareup.tacos.Taco
import com.squareup.tacos.ingredient.Meat
import com.squareup.tacos.internal.iterator
import com.squareup.tacos.internal.minusAssign
fun makeTacoHealthy(taco: Taco) {
 for (ingredient in taco) {
 if (ingredient is Meat) taco -= ingredient
 }
 return taco
}

用%N占位名称

一般来说,我们生成的代码需要相互引用或者自我引用。KotlinPoet中使用%N来引用在KotlinPoet中声明的另一个实体。下面例子中一个方法引用了另一个方法:

kotlin

复制代码

fun byteToHex(b: Int): String {
 val result = CharArray(2)
 result[0] = hexDigit((b ushr 4) and 0xf)
 result[1] = hexDigit(b and 0xf)
 return String(result)
}
fun hexDigit(i: Int): Char {
 return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()
}

在生成上面的代码时,我们需要使用%NhexDigit()方法作为参数传递给byteToHex() 方法:

css

复制代码

val hexDigit = FunSpec.builder("hexDigit")
 .addParameter("i", Int::class)
 .returns(Char::class)
 .addStatement("return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()")
 .build()
val byteToHex = FunSpec.builder("byteToHex")
 .addParameter("b", Int::class)
 .returns(String::class)
 .addStatement("val result = CharArray(2)")
 .addStatement("result[0] = %N((b ushr 4) and 0xf)", hexDigit)
 .addStatement("result[1] = %N(b and 0xf)", hexDigit)
 .addStatement("return String(result)")
 .build()

%N另一个功能是会使用双勾号``自动转义包含非法标识符字符的名称。比如下面的例子使用了Kotlin中的关键字package作为方法名:

less

复制代码

val taco = ClassName("com.squareup.tacos", "Taco")
val packager = ClassName("com.squareup.tacos", "TacoPackager")
val file = FileSpec.builder("com.example", "Test")
 .addFunction(
 FunSpec.builder("packageTacos")
 .addParameter("tacos", LIST.parameterizedBy(taco))
 .addParameter("packager", packager)
 .addStatement("packager.%N(tacos)", packager.member("package"))
 .build()
 )
 .build()

%N会转义名称,确保输出能通过编译:

kotlin

复制代码

package com.example
import com.squareup.tacos.Taco
import com.squareup.tacos.TacoPackager
import kotlin.collections.List
fun packageTacos(tacos: List<Taco>, packager: TacoPackager) {
 packager.`package`(tacos)
}

用%L占位字面量(Literals)

一般来说KotlinPoet中的字符串模板效果都很好。但考虑到我们有时候可能需要在生成的代码中带一些字面量的内容, KotlinPoet 提供了一种类似 String.format()的语法。用%L作为字面量的占位符,工作起来就像String.format()中的%s

kotlin

复制代码

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

%L会不经过任何转义直接将字面量提供给输出文件。%L的参数可以是字符串、原始类型或者之前提到过的一些 KotlinPoet 类型。

代码块中占位符传递参数

代码块有三种方式给占位符传递参数,每个代码块只能使用一种。

相对位置

按顺序给每个占位符传递参数值。下面每个例子,都会生成代码“I ate 3 tacos”:

perl

复制代码

CodeBlock.builder().add("I ate %L %L", 3, "tacos")
指定位置

在占位符前放一个从1开始的整数索引,用指定要使用第几个参数。

perl

复制代码

CodeBlock.builder().add("I ate %2L %1L", "tacos", 3)
命名参数

使用语法%argumentName:X,其中X是对应需要使用的占位符。CodeBlock.addNamed()中需要传入<名称,值>这样的包含所有使用到的参数的映射。参数名称只能使用a-zA-Z0-9,和_并且以小写字母开头。

perl

复制代码

val map = LinkedHashMap<String, Any>()
map += "food" to "tacos"
map += "count" to 3
CodeBlock.builder().addNamed("I ate %count:L %food:L", map)

别名(Type Aliases)

KotlinPoet使用TypeAliasSpec.builder创建别名,支持三种别名类型,简单类型、泛型类型和lambda类型:

scss

复制代码

val k = TypeVariableName("K")
val t = TypeVariableName("T")
val fileTable = Map::class.asClassName()
 .parameterizedBy(k, Set::class.parameterizedBy(File::class))
val predicate = LambdaTypeName.get(
 parameters = arrayOf(t),
 returnType = Boolean::class.asClassName()
)
val helloWorld = FileSpec.builder("com.example", "HelloWorld")
 .addTypeAlias(TypeAliasSpec.builder("Word", String::class).build())
 .addTypeAlias(
 TypeAliasSpec.builder("FileTable", fileTable)
 .addTypeVariable(k)
 .build()
 )
 .addTypeAlias(
 TypeAliasSpec.builder("Predicate", predicate)
 .addTypeVariable(t)
 .build()
 )
 .build()

会生成:

复制代码

package com.example
import java.io.File
import kotlin.Boolean
import kotlin.String
import kotlin.collections.Map
import kotlin.collections.Set
typealias Word = String
typealias FileTable<K> = Map<K, Set<File>>
typealias Predicate<T> = (T) -> Boolean

可调用的引用(Callable References)

KotlinPoet中也支持对构造函数、函数和属性的可调用引用:

  • ClassName.constructorReference()调用构造函数

  • MemberName.reference()调用函数和属性

scss

复制代码

val helloClass = ClassName("com.example.hello", "Hello")
val worldFunction: MemberName = helloClass.member("world")
val byeProperty: MemberName = helloClass.nestedClass("World").member("bye")
val factoriesFun = FunSpec.builder("factories")
 .addStatement("val hello = %L", helloClass.constructorReference())
 .addStatement("val world = %L", worldFunction.reference())
 .addStatement("val bye = %L", byeProperty.reference())
 .build()
FileSpec.builder("com.example", "HelloWorld")
 .addFunction(factoriesFun)
 .build()

会产生:

kotlin

复制代码

package com.example
import com.example.hello.Hello
fun factories() {
 val hello = ::Hello
 val world = Hello::world
 val bye = Hello.World::bye
}

如果顶级类或成员之间有名称上的冲突,可能需要别名导入(见%M一节)。

kotlin-reflect包

为了能够生成K``Type和它的实现类,以及获得kotlin内置反射API没办法获取到的信息,KotlinPoet依赖了kotlin-reflect包。 kotlin-reflect可以读取类信息以获得额外的信息。比如说KotlinPoet 能够从一个带泛型的KType实现类中读取类型参数(一般用于泛型)和它们的型变Variance,也就是in,out和*)。

kotlin-reflect 是一个相对比较大的依赖,所以有时候我们为了节省空间或者简化proguard/R8混淆设置会希望能够从最终执行文件中删掉它(例如一个生成Kotlin代码的Gradle插件)。删掉它是完全可行的,而且删掉之后依然可以使用大部分KotlinPoet的API:

javascript

复制代码

dependencies {
 implementation("com.squareup:kotlinpoet:<version>") {
 exclude(module = "kotlin-reflect")
 }
}

KotlinPoet中依赖kotlin-reflect的主要API是KType.asTypeName()typeNameOf<T>()。如果在没有kotlin-reflect依赖的情况调用的话,很显然会崩溃。

可以替换为显式传递类型参数的方式,并且注释清楚:

kotlin

复制代码

// Replace
// kotlin-reflect needed
val typeName = typeNameOf<List<Int?>>()
// With
// kotlin-reflect not needed
val typeName =
 List::class.asClassName().parameterizedBy(Int::class.asClassName().copy(nullable = true))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值