Search code examples
kotlinandroid-jetpack-composeviewmodelkotlin-stateflow

Jetpack Compose - have a problem with recomposition


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


Solution

  • 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.