iTranslated by AI
Trying out KSP
I've been playing around with KSP and faced various challenges, so I'm writing this memo.
There's also a bit of an anticlimax where I couldn't achieve what I initially set out to do.
Modules using KSP
build.gradle
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}
dependencies {
implementation project(":Wasabi")
ksp project(":Wasabi")
}
That's all.
The module named "Wasabi" is where the KSP processing logic is written.
KSP Core Module
build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm'
id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "com.google.devtools.ksp:symbol-processing-api:1.5.31-1.0.0"
}
That's really it.
Since it's not related to Android, no Android-specific information is included.
META-INF/services

The files within the module are structured like this.
You place files under META-INF/services to specify where the Provider and Processor are located.
This was a point where I struggled because the file names were different from what I saw in official docs and other sources, and it wouldn't work.
Prepare files named com.google.devtools.ksp.processing.SymbolProcessorProvider and javax.annotation.processing.Processor.
The contents of each file are:
sobaya.lib.wasabi.WasabiProvider
sobaya.lib.wasabi.WasabiProcessor
respectively.
Provider
package sobaya.lib.wasabi
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class WasabiProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return WasabiProcessor(environment.codeGenerator, environment.logger)
}
}
This simply returns an instance of the Processor.
Processor
override fun process(resolver: Resolver): List<KSAnnotated> {
if (invoked) {
return emptyList()
}
val functions = ArrayList<String>()
val viewModels = getViewModels(resolver)
val states = getStates(resolver)
states.forEach { (viewModelPath, fieldName) ->
val viewModel = viewModels.find {viewModelPath.contains(it) }
viewModel?.let {
functions.add(makeFunction(viewModel, fieldName))
}
}
val file = codeGenerator.createNewFile(
Dependencies(false),
"sobaya.wasabi",
"StateExtensions"
)
file.write(functions.joinToString("\n").toByteArray())
file.close()
invoked = true
return emptyList()
}
Since it's long, I've only included the main part for now.
functions will contain the strings for the functions you want to create with KSP.
codeGenerator.createNewFile(
Dependencies(false),
"sobaya.wasabi",
"StateExtensions"
)
The strings in functions are written to the file specified above.
In this example, it's written to the sobaya.wasabi.StateExtensions.kt file.
Although omitted here, you can also create text files etc. by specifying an extension in the fourth argument.
Extracting information from class annotations
val viewModelSymbol = resolver.getSymbolsWithAnnotation("sobaya.lib.wasabi.ViewModel")
viewModelSymbol.filterIsInstance<KSClassDeclaration>().forEach {
viewModels.add("${it.packageName.asString()}.${it.simpleName.asString()}")
}
Pass your custom annotation to getSymbolsWithAnnotation to get the locations where the annotation is applied.
filterIsInstance<KSClassDeclaration>: Since we are applying it to classes this time, filter by KSClassDeclaration.
it.packageName.asString() for the package name.
it.simpleName.asString() for the class name.
Extracting information from field annotations
val stateSymbol = resolver.getSymbolsWithAnnotation("sobaya.lib.wasabi.State")
stateSymbol.filterIsInstance<KSPropertyDeclaration>().forEach { it ->
it.annotations.forEach {
val filePath = (it.parent?.location as FileLocation).filePath
val file = File(filePath)
val fieldName = it.parent.toString()
states.add(file.path.replace("/", ".") to fieldName)
}
}
Retrieving annotation information with getSymbolsWithAnnotation is the same as before.
KSPropertyDeclaration: Since we want information about variables this time, we filter by this.
By checking the contents of KSPropertyDeclaration.annotations:
(it.parent?.location as FileLocation).filePath allows you to get which file it's written in and where.
it.parent.toString() allows you to get the variable name.
Example)
@Test
val test = Data()
In this case, test can be retrieved.
Types can also be retrieved, but I forgot to note it down because I didn't use it this time.
Finally

When you rebuild the project, the specified files will be automatically created.
This time, I intended to create a feature to make it easier to manage States in ViewModels using data classes, where writing things like:
state.value = state.value.copy(list = it)
is tedious, and I wanted to simplify it.
However, I failed completely.
But being able to add functionality at build time is interesting, so depending on the idea, the possibilities are endless.
Below is the source code where I achieved what I wanted to do, although I haven't solved issues like code cleanup or files being created under the debug directory.
References
Discussion