Miguel
Miguel

Reputation: 45

LiveData - How to observe changes to List inside object?

I have a Composable, a ViewModel and an object of a User class with a List variable in it. Inside the ViewModel I define a LiveData object to hold the User object and in the Composable I want to observe changes to the List inside the User object but it doesn't seem to work very well.

I understand when you change the contents of a List its reference is the same so the List object doesn't change itself, but I've tried copying the list, and it doesn't work; copying the whole User object doesn't work either; and the only way it seems to work is if I create a copy of both. This seems too far-fetched and too costly for larger lists and objects. Is there any simpler way to do this?

The code I have is something like this:

Composable

@Composable
fun Greeting(viewModel: ViewModel) {
    val user = viewModel.user.observeAsState()

    Column {
        // TextField and Button that calls viewModel.addPet(petName)

        LazyColumn {
            items(user.value!!.pets) { pet ->
                Text(text = pet)
            }
        }
    }
}

ViewModel

class ViewModel {
    val user: MutableLiveData<User> = MutableLiveData(User())

    fun addPet(petName: String){
        val sameList = user.value!!.pets
        val newList = user.value!!.pets.toMutableList()
        newList.add(petName)
        sameList.add(petName)                            // This doesn't work
        user.value = user.value!!.copy()                 // This doesn't work
        user.value!!.pets = newList                      // This doesn't work
        user.value = user.value!!.copy(pets = newList)   // This works BUT...
    }
}

User

data class User(
    // Other variables
    val pets: MutableList<String> = mutableListOf()
)

Upvotes: 2

Views: 2811

Answers (2)

Phil Dukhov
Phil Dukhov

Reputation: 87635

MutableLiveData will only notify view when it value changes, e.g. when you place other value which is different from an old one. That's why user.value = user.value!!.copy(pets = newList) works.

MutableLiveData cannot know when one of the fields was changed, when they're simple basic types/classes.

But you can make pets a mutable state, in this case live data will be able to notify about changes. Define it like val pets = mutableStateListOf<String>().


I personally not a big fan of live data, and code with value!! looks not what I'd like to see in my project. So I'll tell you about compose way of doing it, in case your project will allow you to use it. You need to define both pets as a mutable state list of strings, and user as a mutable state of user.

I suggest you read about compose states in the documentation carefully.

Also note that in my code I'm defining user with delegation, and pets without delegation. You can use delegation only in view model, and inside state holders you cannot, othervise it'll become plain objects at the end.

@Composable
fun TestView() {
    val viewModel = viewModel<TestViewModel>()
    Column {
        // TextField and Button that calls viewModel.addPet(petName)
        var i by remember { mutableStateOf(0) }

        Button(onClick = { viewModel.addPet("pet ${i++}") }) {
            Text("add new pet")
        }

        LazyColumn {
            items(viewModel.user.pets) { pet ->
                Text(text = pet)
            }
        }
    }
}

class User {
    val pets = mutableStateListOf<String>()
}

class TestViewModel: ViewModel() {
    val user by mutableStateOf(User())

    fun addPet(petName: String) {
        user.pets.add(petName)
    }
}

Upvotes: 3

Francesc
Francesc

Reputation: 29260

Jetpack Compose works best with immutable objects, making a copy with modern Android and ART is not the issue that it was in the past.

However, if you do not want to make a whole copy of your object, you could add a dummy int to it and then mutate that int when you also mutate the list, but I strongly urge you to consider immutability and instantiate a new User object instead.

Upvotes: 0

Related Questions