2

I am using a ViewModel to store data. The ViewModel has a StateFlow class that list of Users. There is also a donut:Int field.

In Compose, when I click on the Donut-button, the variable "donut" is incremented and the recomposition is successful - the data on the button "Donut" changes immediately. On the Users buttons, when I click, the data in the User list changes (looked through logcat), but the recomposition does not occur. But if after that I press the Donut-button, then the user data instantly changes on the screen to the changed ones.my screen

My MainViewModel:

class MainViewModel() : ViewModel() {

    private var _usersClass = MutableStateFlow(UsersClass())
    val usersClass: StateFlow<UsersClass> = _usersClass.asStateFlow()

    fun addDonut(int: Int) {
        _usersClass.update {
            it.copy(
                donuts = int
            )
        }
    }

    fun addAgeForAll(){
        val newList = _usersClass.value.userList
        newList.value.forEach(){a->a.addAge()}
        _usersClass.update{
            it.copy(
                userList = newList
            )
        }
    }
}

My UsersClass:

data class UsersClass(
    var donuts: Int = 0,

    private var _userList: MutableStateFlow<MutableList<User>> = MutableStateFlow(initData.items.toMutableList()),
    var userList: StateFlow<List<User>> = _userList.asStateFlow()
)

data class User(
    var username: String = "John Doe",
    var age: Int = 18
) {
    fun addAge() {
        age++
    }
}

object initData {
    var items = arrayListOf(
        User(username = "Elvis", age = 18),
        User(username = "Sunny", age = 19),
        User(username = "Mary", age = 18)
    )
}

MainActivity:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]

        setContent {
            MainScreen(
                mainViewModel = mainViewModel,
                onClickButtonAddDonut = {
                    mainViewModel.addDonut(mainViewModel.usersClass.value.donuts + 1)
                }
            )
        }
    }
}

@Composable
fun MainScreen(
    mainViewModel: MainViewModel,
    onClickButtonAddDonut: () -> Unit,
) {
    val class1 = mainViewModel.usersClass.collectAsState()
    Column {
        LazyColumn {
            itemsIndexed(class1.value.userList.value) { _, item ->
                Button(
                    onClick = { mainViewModel.addAgeForAll() }
                ) {
                    Text(text = item.toString())
                }

            }
        }

        Button(
            modifier = Modifier,
            onClick = { onClickButtonAddDonut() },
            content = {
                Text(text = "donuts= ${class1.value.donuts}")
            })

    }
}

Why is recomposition not happening? What am I doing wrong? I can't figure it out for a week :( Thx

Ten
  • 33
  • 3
  • If you use a **LiveData** instead of a **StateFlow**, then the behavior is similar. – Ten Jun 22 '23 at 11:04
  • Yes, it is similar.

    private var _userList : MutableList = initData.items,
    var userList: List = _userList
    – Ten Jun 22 '23 at 11:35

1 Answers1

4

The issue is here,

fun addAge() {
    age++
}

This doesn't notify Compose that anything has changed as it is mutating the value of age directly.

In general, the data sent through a state flow should be immutable. To do this you can change User to be,

data class User(
    val username: String = "John Doe",
    val age: Int = 18
)

and UserClass to be,

data class UsersClass(
    val donuts: Int = 0,
    val userList: List<User> = emptyList()
)

Note the use of val instead of var.

After these changes, method on the model could be,

fun addAgeForAll(){
    val newList = _usersClass.value.userList.map {
      it.copy(age = it.age + 1)
    }
    _usersClass.update{
        it.copy(
            userList = newList
        )
    }
}

I recommend you keep the model simple, like this, until you know that an additional flow is required to keep from skipping frames.

chuckj
  • 27,773
  • 7
  • 53
  • 49
  • Thank you very much! It works! – Ten Jun 27 '23 at 18:41
  • Do you mean that only the **.update** function of the **MutableStateFlow**-field activates a recomposition? @chuckj – Ten Jun 28 '23 at 06:32
  • 1
    @Ten no, its not about the `.update` function. Its about a **new List** that get emitted to the `StateFlow` (and therefore to the Composable `State`). See also this SO question: https://stackoverflow.com/a/71633873. So in a nutshell, you don't change the object from the point of the Composable Compiler. You are still emitting **the exact same** `MutableList` object. Yes, the content of the list changed, but not the `MutableList` object itself. Does this make sense? – StefMa Jul 05 '23 at 09:18