An Annotation to store data class @AsState
Well I am still not sure about wether it is fine to simply .copy(changedValue = "...")
a large data class or if this is inefficient because it triggers unecessary recompositions.
In any case just to be safe (and cause I think its cleaner) I wrote an AnnotationProcessor
that takes my data classes
and creates both a mutable and immutable version of the class holding all properties as state. It supports both lists and maps but is shallow (meaning that it wont repeat the same process for nested classes). Here an example of the class with the annotation and the result.
please let me know if you consider this to be usefull in order to track state more cleanly when a data class is displayed/edited in a composable (and also if you dont)
The original class
@AsState
data class Test(val name:String, val age:Int, val map:HashMap<String,Int>, val list:ArrayList<String>)
The mutable verion of the class with a custonm constructor and rootClass getter
public class TestMutableState {
public val name: MutableState<String>
public val age: MutableState<Int>
public val map: SnapshotStateMap<String, Int>
public val list: SnapshotStateList<String>
public constructor(rootObject: Test) {
this.name=mutableStateOf(rootObject.name)
this.age=mutableStateOf(rootObject.age)
this.map=rootObject.map.map{Pair(it.key,it.value)}.toMutableStateMap()
this.list=rootObject.list.toMutableStateList()
}
public fun getTest(): Test = Test(name = this.name.value,
age = this.age.value,
map = HashMap(this.map),
list = ArrayList(this.list),
)
}
The immutable version that can be public in the ViewModel
public class TestState {
public val name: State<String>
public val age: State<Int>
public val map: SnapshotStateMap<String, Int>
public val list: SnapshotStateList<String>
public constructor(mutableObject: TestMutableState) {
this.name=mutableObject.name
this.age=mutableObject.age
this.map=mutableObject.map
this.list=mutableObject.list
}
}
TL;DR
Next I am pasting the source code for my annotation processor so you can implement it. I basically followed this article and implemented some of my own changes based on arduous googling. I might make this a module in the future so that other people can more easily implement this in their projects i there is any interest:
Annotation class
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class AsState
Annotation processor
@AutoService(Processor::class)
class AnnotationProcessor : AbstractProcessor() {
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(AsState::class.java.name)
}
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(AsState::class.java)
.forEach {
if (it.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated")
return true
}
processAnnotation(it)
}
return false
}
@OptIn(KotlinPoetMetadataPreview::class, com.squareup.kotlinpoet.DelicateKotlinPoetApi::class)
private fun processAnnotation(element: Element) {
val className = element.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(element).toString()
val kmClass = (element as TypeElement).toImmutableKmClass()
//create vessel for mutable state class
val mutableFileName = "${className}MutableState"
val mutableFileBuilder= FileSpec.builder(pack, mutableFileName)
val mutableClassBuilder = TypeSpec.classBuilder(mutableFileName)
val mutableConstructorBuilder= FunSpec.constructorBuilder()
.addParameter("rootObject",element.asType().asTypeName())
var helper="return ${element.simpleName}("
//create vessel for immutable state class
val stateFileName = "${className}State"
val stateFileBuilder= FileSpec.builder(pack, stateFileName)
val stateClassBuilder = TypeSpec.classBuilder(stateFileName)
val stateConstructorBuilder= FunSpec.constructorBuilder()
.addParameter("mutableObject",ClassName(pack,mutableFileName))
//import state related libraries
val mutableStateClass= ClassName("androidx.compose.runtime","MutableState")
val stateClass=ClassName("androidx.compose.runtime","State")
val snapshotStateMap= ClassName("androidx.compose.runtime.snapshots","SnapshotStateMap")
val snapshotStateList=ClassName("androidx.compose.runtime.snapshots","SnapshotStateList")
fun processMapParameter(property: ImmutableKmValueParameter) {
val clName =
((property.type?.abbreviatedType?.classifier) as KmClassifier.TypeAlias).name
val arguments = property.type?.abbreviatedType?.arguments?.map {
ClassInspectorUtil.createClassName(
((it.type?.classifier) as KmClassifier.Class).name
)
}
val paramClass = ClassInspectorUtil.createClassName(clName)
val elementPackage = clName.replace("/", ".")
val paramName = property.name
arguments?.let {
mutableClassBuilder.addProperty(
PropertySpec.builder(
paramName,
snapshotStateMap.parameterizedBy(it), KModifier.PUBLIC
)
.build()
)
}
arguments?.let {
stateClassBuilder.addProperty(
PropertySpec.builder(
paramName,
snapshotStateMap.parameterizedBy(it), KModifier.PUBLIC
)
.build()
)
}
helper = helper.plus("${paramName} = ${paramClass.simpleName}(this.${paramName}),\n")
mutableConstructorBuilder
.addStatement("this.${paramName}=rootObject.${paramName}.map{Pair(it.key,it.value)}.toMutableStateMap()")
stateConstructorBuilder
.addStatement("this.${paramName}=mutableObject.${paramName}")
}
fun processListParameter(property: ImmutableKmValueParameter) {
val clName =
((property.type?.abbreviatedType?.classifier) as KmClassifier.TypeAlias).name
val arguments = property.type?.abbreviatedType?.arguments?.map {
ClassInspectorUtil.createClassName(
((it.type?.classifier) as KmClassifier.Class).name
)
}
val paramClass = ClassInspectorUtil.createClassName(clName)
val elementPackage = clName.replace("/", ".")
val paramName = property.name
arguments?.let {
mutableClassBuilder.addProperty(
PropertySpec.builder(
paramName,
snapshotStateList.parameterizedBy(it), KModifier.PUBLIC
)
.build()
)
}
arguments?.let {
stateClassBuilder.addProperty(
PropertySpec.builder(
paramName,
snapshotStateList.parameterizedBy(it), KModifier.PUBLIC
)
.build()
)
}
helper = helper.plus("${paramName} = ${paramClass.simpleName}(this.${paramName}),\n")
mutableConstructorBuilder
.addStatement("this.${paramName}=rootObject.${paramName}.toMutableStateList()")
stateConstructorBuilder
.addStatement("this.${paramName}=mutableObject.${paramName}")
}
fun processDefaultParameter(property: ImmutableKmValueParameter) {
val clName = ((property.type?.classifier) as KmClassifier.Class).name
val paramClass = ClassInspectorUtil.createClassName(clName)
val elementPackage = clName.replace("/", ".")
val paramName = property.name
mutableClassBuilder.addProperty(
PropertySpec.builder(
paramName,
mutableStateClass.parameterizedBy(paramClass), KModifier.PUBLIC
).build()
)
stateClassBuilder.addProperty(
PropertySpec.builder(
paramName,
stateClass.parameterizedBy(paramClass),
KModifier.PUBLIC
).build()
)
helper = helper.plus("${paramName} = this.${paramName}.value,\n")
mutableConstructorBuilder
.addStatement(
"this.${paramName}=mutableStateOf(rootObject.${paramName}) "
)
stateConstructorBuilder
.addStatement("this.${paramName}=mutableObject.${paramName}")
}
for (property in kmClass.constructors[0].valueParameters) {
val javaPackage = (property.type!!.classifier as KmClassifier.Class).name.replace("/", ".")
val javaClass=try {
Class.forName(javaPackage)
}catch (e:Exception){
String::class.java
}
when{
Map::class.java.isAssignableFrom(javaClass) ->{ //if property is of type map
processMapParameter(property)
}
List::class.java.isAssignableFrom(javaClass) ->{ //if property is of type list
processListParameter(property)
}
else ->{ //all others
processDefaultParameter(property)
}
}
}
helper=helper.plus(")") //close off method
val getRootBuilder= FunSpec.builder("get$className")
.returns(element.asClassName())
getRootBuilder.addStatement(helper.toString())
mutableClassBuilder.addFunction(mutableConstructorBuilder.build()).addFunction(getRootBuilder.build())
stateClassBuilder.addFunction(stateConstructorBuilder.build())
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
val mutableFile = mutableFileBuilder
.addImport("androidx.compose.runtime", "mutableStateOf")
.addImport("androidx.compose.runtime","toMutableStateMap")
.addImport("androidx.compose.runtime","toMutableStateList")
.addType(mutableClassBuilder.build())
.build()
mutableFile.writeTo(File(kaptKotlinGeneratedDir))
val stateFile = stateFileBuilder
.addType(stateClassBuilder.build())
.build()
stateFile.writeTo(File(kaptKotlinGeneratedDir))
}
}
gradle annotation
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
gradle processor
plugins {
id 'kotlin'
id 'kotlin-kapt'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':annotations')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10"
// https://mvnrepository.com/artifact/com.squareup/kotlinpoet
implementation 'com.squareup:kotlinpoet:1.10.2'
implementation "com.squareup:kotlinpoet-metadata:1.7.1"
implementation "com.squareup:kotlinpoet-metadata-specs:1.7.1"
implementation "com.google.auto.service:auto-service:1.0.1"
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-metadata-jvm
implementation "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.4.2"
implementation 'org.json:json:20211205'
kapt "com.google.auto.service:auto-service:1.0.1"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}