19

The below two examples simply add an 'a' to a given default value. The compose_version used is 1.0.0-alpha03 which is the latest as of today (to my knowledge).

This example is most similar to most examples I've found during my research.

Example 1

@Composable
fun MyScreen() {
    val (name, setName) = remember { mutableStateOf("Ma") }

    Column {
        Text(text = name) // 'Ma'
        Button(onClick = {
                setName(name + "a") // change it to 'Maa'
        }) {
            Text(text = "Add an 'a'")
        }
    }
}

However, this isn't always practical. Say for example, the data was more complex than a single field. A class for instance, or even a Room data class.

Example 2

// the class to be modified
class MyThing(var name: String = "Ma");


@Composable
fun MyScreen() {
    val (myThing, setMyThing) = remember { mutableStateOf(MyThing()) }

    Column {
        Text(text = myThing.name) // 'Ma'
        Button(onClick = {
                var nextMyThing = myThing
                nextMyThing.name += "a" // change it to 'Maa'
                setMyThing(nextMyThing)
        }) {
            Text(text = "Add an 'a'")
        }
    }
}

Of course, Example 1 works, but Example 2 does not. Is this a simple error on my part, or am I missing the larger picture about how I should go about modifying this class instance?

EDIT:

I have sort of found a way to make this work, but it seems inefficient. It does line up with how React manages state however, so maybe it is the correct way to do it.

The issue in Example 2 quite clearly is that myNextThing is not a copy of the original myThing, but rather a reference to it. Just like React, Jetpack Compose seems to want an entirely new object when modifying the state. This can be done in one of two ways:

  1. Creating a new instance of the MyThing class, changing what one needs to change, and then calling setMyThing() with the new class instance
  2. Changing class MyThing to data class MyThing and using the copy() function to create a new instance with the same properties. Then, change the desired property and call setMyThing(). This is the best way in the context of my question given that I explicitly stated that I would like to use this to modify the data on a given data class used by Android Room.

Example 3 (functional)

// the class to be modified
data class MyThing(var name: String = "Ma");


@Composable
fun MyScreen() {
    val (myThing, setMyThing) = remember { mutableStateOf(MyThing()) }

    Column {
        Text(text = myThing.name) // 'Ma'
        Button(onClick = {
                var nextMyThing = myThing.copy() // make a copy instead of a reference
                nextMyThing.name += "a" // change it to 'Maa'
                setMyThing(nextMyThing)
        }) {
            Text(text = "Add an 'a'")
        }
    }
}
Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
foxtrotuniform6969
  • 3,527
  • 7
  • 28
  • 54

6 Answers6

11

Indeed, it appears to me that the best way to go about this is to copy() a data class.

In the specific case of using remember() of a custom data class, that probably is indeed the best option, though it can be done more concisely by using named parameters on the copy() function:

// the class to be modified
data class MyThing(var name: String = "Ma", var age: Int = 0)

@Composable
fun MyScreen() {
  val (myThing, myThingSetter) = remember { mutableStateOf(MyThing()) }

  Column {
    Text(text = myThing.name)
    // button to add "a" to the end of the name
    Button(onClick = { myThingSetter(myThing.copy(name = myThing.name + "a")) }) {
      Text(text = "Add an 'a'")
    }
    // button to increment the new "age" field by 1
    Button(onClick = { myThingSetter(myThing.copy(age = myThing.age + 1)) }) {
      Text(text = "Increment age")
    }
  }
}

However, we are going to still update viewmodels and observe results from them (LiveData, StateFlow, RxJava Observable, etc.). I expect that remember { mutableStateOf() } will be used locally for data that is not yet ready to submit to the viewmodel yet needs multiple bits of user input and so needs to be represented as state. Whether or not you feel that you need a data class for that or not is up to you.

Is this a simple error on my part, or am I missing the larger picture about how I should go about modifying this class instance?

Compose has no way of knowing that the object changed, and so it does not know that recomposition is needed.

On the whole, Compose is designed around reacting to streams of immutable data. remember { mutableStateOf() } creates a local stream.

An alternative approach, however, would be most welcome.

You are not limited to a single remember:

@Composable
fun MyScreen() {
  val name = remember { mutableStateOf("Ma") }
  val age = remember { mutableStateOf(0) }

  Column {
    Text(text = name.value)
    // button to add "a" to the end of the name
    Button(onClick = { name.value = name.value + "a"}) {
      Text(text = "Add an 'a'")
    }
    // button to increment the new "age" field by 1
    Button(onClick = { age.value = age.value + 1 }) {
      Text(text = "Increment age")
    }
  }
}
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    I am currently struggling with the last part of your anwer. I am guessing you propose using multiple state objects to reflect each of the data class'es properties in order to avoid the recomposition of the entire composable tree when only one property has changed. But for bigger data classes this really bloats my code, with several lines dedicated to making each propperty of a data class observable. i wonder if one could create a transformation of a data class using reflection so that all of its properties were exposed as state. kind of turning it into a state holder version of itself – quealegriamasalegre Feb 18 '22 at 20:54
  • This becomes a nightmare when you have deeply nested objects. – dessalines May 13 '23 at 13:46
11

Ok so for anyone wondering about this there is an easier way to resolve this issue. When you define you mutable state property like this:

//There is a second paremeter wich defines the policy of the changes on de state if you
//set this value to neverEqualPolicy() you can make changes and then just set the value
class Vm : ViewModel() {
 val dummy = mutableStateOf(value = Dummy(), policy= neverEqualPolicy())

 //Update the value like this 
 fun update(){
 dummy.value.property = "New value"
//Here is the key since it has the never equal policy it will treat them as different no matter the changes
 dummy.value = dummy.value
 }
}

For more information about the available policies: https://developer.android.com/reference/kotlin/androidx/compose/runtime/SnapshotMutationPolicy

Augusto Alonso
  • 334
  • 3
  • 5
4

Indeed, it appears to me that the best way to go about this is to copy() a data class.

A full and useful example using reflection (to allow the modification of my different types of properties might look like this:

// the class to be modified
data class MyThing(var name: String = "Ma", var age: Int = 0);


@Composable
fun MyScreen() {
    val (myThing, setMyThing) = remember { mutableStateOf(MyThing()) }

    // allow the `onChange()` to handle any property of the class
    fun <T> onChange(field: KMutableProperty1<MyThing, T>, value: T) {
        // copy the class instance
        val next = myThing.copy()
        // modify the specified class property on the copy
        field.set(next, value)
        // update the state with the new instance of the class
        setMyThing(next)
    }

    Column {
        Text(text = myThing.name)
        // button to add "a" to the end of the name
        Button(onClick = { onChange(MyThing::name, myThing.name + "a") }) {
            Text(text = "Add an 'a'")
        }
        // button to increment the new "age" field by 1
        Button(onClick = { onChange(MyThing::age, myThing.age + 1) }) {
            Text(text = "Increment age")
        }
    }
}

While it may be that instantiating a copy of the class in state every time the button is click (or the keyboard is pressed in a real-world use case with a TextField instead of a button) may be a bit wasteful for larger classes, it generally seems as though the Compose framework would prefer this approach. As stated, this falls in line with the way that React does things: state is never modified or appended, it is always completely replaced.

An alternative approach, however, would be most welcome.

foxtrotuniform6969
  • 3,527
  • 7
  • 28
  • 54
  • 1
    but imagine you have a big data class (like a user profile class) and you are showing the user profile on the screen. this approach will trigger recomposition of your entire composable tree while maybe only a single textfield has actually changed. This should have performance implications right? – quealegriamasalegre Feb 18 '22 at 20:36
3

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. I know from experience that it can cause some tedious code when dealing with changing hashmaps and lists inside of data classes. On the one hand what @CommonsWare mentioned as an alternative approach really sounds like the right way to go: i.e. tracking every property of a data class that can change as State independently. Yet this makes my code and ViewModels incredibly verbose. And just imagine adding a new property to a data class; you then need to create a mutable and an immutable stateholder for this property as well its just needlesly tedious.

My solution: I went in a similar direction as what @foxtrotuniform6969 was trying to do. 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 a Test.class with the annotation and the resulting generated classes. As you can see you can easily instantiate the state holder classes using the original data class and conversely harvest the modified data class from the state holder class.

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
}
quealegriamasalegre
  • 2,887
  • 1
  • 13
  • 35
  • Thanks for sharing your interesting work, @quealegriamasalegre. However, it seems to me that delegated properties kinda defeat the purpose of your annotation processor, unless you need those properties to be initialized in the constructor, in which case your solution would save some slight repetition. You might be interested in my answer, the second part of it especially. – Davide Cannizzo Apr 25 '23 at 21:33
  • I also found your take on possible performance penalty inherent to the immutable approach particularly interesting, @quealegriamasalegre. I cited it in my answer (with credit, of course!) and added my considerations on that, too. – Davide Cannizzo Apr 25 '23 at 21:49
2

For one, property delegation really makes mutating MutableStates more convenient (example follows).

Also, using immutable data classes is the most straightforward and best solution overall, by far; immutability does in fact give useful guarantees. However, there is an alternative for those situations where this is not appropriate; which is later covered in the answer.

Normally, you'd want to just go for the immutable way, though — and simply use copy to generate mutated instances and update a MutableState to the most current instance, e.g. (modified version of the one in the updated question):

@Immutable
data class MyThing(var name: String)

@Composable
fun MyScreen() {

    var myThing by remember { mutableStateOf(MyThing(name = "Ma")) }

    Column {

        Text(text = myThing.name)

        Button(
            onClick = {
                myThing = myThing.run { copy(name = "${name}a" }
            }
        ) {
            Text(text = "Add an 'a'")
        }
    }
}

Note as well that the @Immutable annotation on MyThing gives Compose useful guarantees that might allow for greater optimization.

Be aware that the copy method does shallow copying, so I'd suggest employing deep immutability (that is, make sure that the data class only has read-only properties of immutable types).


However, immutability with atomic updates like this only works for classes with structural identity (which is why it especially makes sense to use data classes).

Or, it might just be a performance issue in some rare cases, like if used for every frame in an animation or such scenarios. This is rare, though. Don't waste time with premature optimization. If you're particularly interested in this (or are having a performance problem right now), check an additional note at the end of this post.

Anyhow, here's the alternative. And that is... Well, as simple as having a class that holds MutableStates as opposed to having a MutableState that holds an instance of a class:

// Not a data class, referential identity.
@Stable
class MyThing {
    // Using delegated properties for ease of use.
    var name by mutableStateOf(value = "Ma")
    // Optionally add any more properties like name.
}

@Composable
fun MyScreen() {

    // Remember the same mutable instance of MyThing.
    val myThing = remember { MyThing(name = "Ma") }

    Column {

        Text(text = myThing.name)

        Button(
            onClick = {
                // Simply mutate a property of the same instance.
                myThing.name += "a"
            }
        ) {
            Text(text = "Add an 'a'")
        }
    }
}

Note that the Snapshot system and the Compose Applier together are smart enough to properly schedule recompositions. Therefore, having multiple properties as separate MutableStates in a single class and mutating many of them at the same time does not cause unnecessary compositions, effectively making those mutations almost atomic.


A note on performance:

Actually, @quealegriamasalegre had the best point on possible performance penalty of the immutable approach when dealing with larger objects (refer to their answer), in that every site that has, as its dependency, a MutableState of such data class would be invalidated upon mutations of said state, effectively causing possible excess recompositions.

However, let me note that, at some point down the tree of @Composables, the state is probably destructured, as in, say, a Text would only get MyThing.name. Therefore, Text would not be recomposed if name is unchanged (because of donut-hole skipping), hence making this performance consideration on immutability rather insignificant in most cases (graphics are in fact orders of magnitude heavier than composition per se). That is, unless you have a very deep tree with many branches that depend on instances of a huge data class, in which case I'd question the sanity of the developer (LOL).

Davide Cannizzo
  • 2,826
  • 1
  • 29
  • 31
  • I also questioned my sanity hahahah. my class was representing a map of english courses consisting of multiple parts that in turn had multiple milestones, that had a bunch of properties... Course>part>milestone>property so a wonderful nested hashmap that I was getting from firebase and I needed to track the state of every branch independently. the problem is that for such an object `.copy()` doesnt cut it, as copying an object that contains a hashmap actually returns the same instance of that hashmap even if its contents have changed. – quealegriamasalegre Apr 26 '23 at 15:03
  • The problem that my annotation processor solves is having to decompose a deeply nested data class that contains maps and lists in order to track its state. with my @State annotation you are basically just saving yourself the boilerplate of having to do that for every list and map in a class and also being able to change the state of a single property in a class in a less verbose way. That being said I would say a weakness in my approach is that I made it shallow i.e. it doesnt really address cases where there are many nested layers of maps and lists in a dataclass – quealegriamasalegre Apr 26 '23 at 15:12
  • @quealegriamasalegre, actually, there are `mutableStateListOf` and `mutableStateMapOf` specifically to handle state changes in lists and maps. – Davide Cannizzo Apr 26 '23 at 21:54
  • 1
    I know, If you look at my annotation processor it turns your lists and maps into `SnapshotStateList/Map` which is what `mutableStateMapOf` returns. In my approach you dont have to explicitly convert the lists and maps in your data classes in the viewmodel because a suitable state-holding-class is created for you automatically at compile time – quealegriamasalegre Apr 27 '23 at 08:58
  • @quealegriamasalegre, oh yeah, I didn't notice that at first. I wasn't thinking about the need to work with `ViewModel` because lately I'm ditching all that stuff for Kotlin `StateFlow`s and Compose `State`s. This also allows for cross-platform Compose applications (Jetbrain's Compose Multiplatform allows for building desktop apps as well, and there are beta releases for Web and iOS, too). Unfortunately, not everyone is working on greenfield projects, though (LOL). – Davide Cannizzo Apr 27 '23 at 13:28
0

I'm doing it like this using view model, I'm not sure about how performant is this approach, but it saves me many lines of codes if the object has many properties that change, here I added another property 'surname'.

Data class:

data class MyThing(var name: String = "Ma", var surname: String = "Foo");

View Model class:

class MyThingVM: ViewModel() {
   
    // It's good practice to modify values only by the owner (viewmodel)
    private val _myThing= mutableStateOf(MyThing())
    val myThing: MyThing
       get() = _myThing.value

    fun onThingValueChange(thingCopy: MyThing){
        _myThing.value = thingCopy
    }
}

Composable:

@Composable
fun MyScreen(val vm: MyThingVM = viewModel()): {

    val myThing = vm.myThing

    Column {

        Text(text = myThing.name)
        Text(text = myThing.surname)

        Button(
            onClick = {
                // make a copy, modify property and pass it to viewModel
                var thingCopy = myThing.copy()
                    thingCopy.name = myThing.name + "a"
                    vm.onThingValueChange(thingCopy)
            }
        ) {
            Text(text = "Add an 'a' to name")
        }

        Button(
            onClick = {
                var thingCopy = myThing.copy()
                    thingCopy.surname= myThing.surname + "o"
                    vm.onThingValueChange(thingCopy)
            }
        ) {
            Text(text = "Add an 'o' to surname")
        }
    }
}
krazer
  • 1
  • 1