My first Kotlin Symbol Processing Tool for Android
Google recently posted new Annotation Processing Tool for Kotlin: KSP
According to the post, Kotlin Symbol Processing Tool is 2x faster than existed APT. It also supports simplified api but powerful.
Currently you can see many works related to KSP in AOSP. By all logs, I guess Google is rooting KSP more to settle down itself.
Eventually I decided tobuilt a specific KSP tool in my team : Grouping Moshi JsonAdapter Factory whichis for generated Moshi JsonAdapters.
Disclaimer: I am not professional Annotation Processing tool builder. I don’t know how much it is easier than APT. Also even I don’t understand KSP fully. All code are based on Kotlin and Gradle Kotlin DSL in my posting
Background of requirements
When we communicate with server, we deliver many payloads. the payload type is mostly JSON. we sent and received lots of JSON payloads via HTTP.
For JSON, we are using Moshi
For HTTP, we are using Retrofit
In our case, we have lots of data classes to be mapped and each data classes generated JsonAdapter. But it required to write JsonAdapterFactory to inject Moshi. So we have to write them manually.
and then inject above to Moshi and Retrofit like this
Every single time, we have to write JsonAdapterFactory manually and add it to Moshi builder.
Yeah, It is very unproductive works. So I decided to build our APT to generate JsonAdapterFactory automatically. But KSP seems more simple than APT
1. KSP module config
In KSP tool module, you simply add KSP api dependency
implementation("com.google.devtools.ksp:symbol-processing-api:1.4.30-1.0.0-alpha02")
2. SymbolProcessor
We are going to use SymbolProcessor as Google provided sample project.
The SymbolProcessor has 4 interfaces
fun init(
options: Map<String, String>,
kotlinVersion: KotlinVersion,
codeGenerator: CodeGenerator,
logger: KSPLogger
)fun process(resolver: Resolver): List<KSAnnotated>fun finish()fun onError() {}
the lifecycle order is init(), process() and then finish if no error.
What we are doing is this
- in init() : Read Gradle script property. Set codeGenerator to member field
- in process() : Collect
@JsonClass(generateAdapter = true)
annotated class - in finish() : Generate Grouped JsonAdapterFactory
3. Init()
As explained, we set necessary arguments to member field.
4. process()
During processing, we look up @JsonClass
annotated class and will check either it has generateAdapter = true
.
5. CustomKSVisitor.kt
KSVisitor is to help reading each KSNode information.
KSVisitor has lots of interfaces. it will visit only each scoped functions.
class KSVisitor<D, R> {
fun visitFile(file: KSFile, data: D): R
fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: D): R
fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: D): R
// and so on.. argument, property getter/setter
}
In #4, we filter out if symbol is KSClassDeclaration
and accept CustomVisitor
to look up inside. So the CustomVisitor
only visit visitClassDeclaration()
. If we filter out KSFunctionDeclaration
, then the visitor calls only visitFunctionDeclaration()
.
In CustomKSVisitor, we will only filter out @JsonClass(generateAdapter = true)
and collect class information to targets
which is cache in SymbolProcessor
6. finish()
Before finishing KSP, we will generate GroupedJsonAdapterFactory
.
First step, we should decide file package and name. Seconds step is to decide dependency files to identify incremental build.
val packageName = "com.example"
val outputStream: OutputStream = codeGenerator.createNewFile(
dependencies = Dependencies(
true,
*targets.map { it.containingFile!! }
.toTypedArray()
),
packageName = packageName,
fileName = "GroupJsonAdapterFactory"
)
In sample, I put dependencies on collected classes’ files. codeGenerator.createNewFile()
returns FileOutputStream. we will use the outputStream
to write file
Last part to writing file.
outputStream.write("""
|package $packageName
|
|import com.squareup.moshi.JsonAdapter
|import com.squareup.moshi.Moshi
|import com.squareup.moshi.Types
|// import target classes & target classes json adapter
|class GroupJsonAdapterFactory: JsonAdapter.Factory {
| override fun create(): JsonAdapter<*> {
| // declare logics | }
|}""".trimMargin().toByteArray())
outputStream.close()
7. Resources/META-INF.services
In META-INF.services, we should expose our SymboleProcessor explicitly.
com.example.CustomSymbolProcessor
It is done for KSP module. when you compile with this KSP module, then you can see generated file of GoupJsonAdapterFactory
Let’s move on consumer module
7. Consumer module
Add KSP classpath and use ksp like kapt.
8. Wth??? My IDEA isn’t aware of KSP generated file
Yes currently, KSP generated files isn’t indexed on IDE. To use it, you should do extra work in Gradle script.
This is a known issue now.
Description : https://github.com/google/ksp#development-status
Issue : https://github.com/google/ksp/issues/37
So you should declare sourceSets manually. KSP is still in alpha. I hope Google fixes it.
Tip
If you are doing multi module project, Duplicated class name to generate file is not good idea. So we use this
// consumer build.gradle.kts
ksp {
arg("projectName", project.name)
}// SymbolProcessor.kt
private lateinit var projectName: Stringfun init(options: Map<String, String>) {
projectName = options["projectName"]
}fun finish() { outputStream.write("""
|class ${projectName}JsonAdapterFactory
""")
}
Then you can generate different class name on each module.
Conclusion
I have no idea KSP is really faster or easier than APT. Because I never write APT code generator before. but KSP is really simple and fast enough to use. And I am happy to use.
Check out Repo
Reference
Google/KSP
Github : https://github.com/google/ksp
Sample for Android : google/ksp/playground-android
KSP adoption in AOSP : https://android-review.googlesource.com/q/ksp
Caution.
If you are using KSP in consumer module with Android DataBinding, Android DataBinding may not work. At least it happens in my project.
Update::
com.sqaureup.moshi.internal.Util.generatedAdapter()
is covering my work and internally it has ProguardConfiguration
to prevent obfuscation of class also. Please refer this only for KSP usages. lol