10
votes

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'")
        }
    }
}
3

3 Answers

3
votes

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.

3
votes

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")
    }
  }
}
2
votes

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