Composable doesn't recompose although LiveData changes - android

I simply want to add a Node to a List when the user clicks a button and display it in a composable LazyColumn.
Here is how I thought it would work:
VieModel:
private val _nodeList: MutableLiveData<MutableList<Node>> = MutableLiveData()
val nodeList: LiveData<MutableList<Node>> = _nodeList
fun onNodeListChange(newNode: Node){
_nodeList.value?.add(newNode)
}
and in my Composable I try to observe it by calling:
val vm = getViewModel<MainViewModel>()
val nodeList: List<Node> by vm.nodeList.observeAsState(listOf())
In the onClick of the Button I call:
val newNode = Node(title, body)
vm.onNodeListChange(newNode)
The node gets added to the nodeList in the ViewModel but the Composable wont recompose.
What am I doing wrong?

Your LiveData is not changing. You are just adding a new item to the list stored within the LiveData. For a LiveData to notify it's observers, the object returned by livedata.getValue() must occupy a different memory space from the one supplied through livedata.setValue() One quick and dirty solution would be:
_nodeList.value = _nodeList?.value?.toMutableList()?.apply{add(newNode)}
In Jetpack compose you probably want to use a SnapShotStateList for an observable list instead of a LiveData<List<T>>

Related

Is there a better way to update text in jetpack compose?

Is there a better way to update the text value here by the value from the database?
#Composable
private fun DisplayShops() {
var shopid by remember { mutableStateOf("")}
SideEffect {
val value = GlobalScope.async {
val res = withContext(Dispatchers.Default) {
getDbData() // this gets the database data
delay(1000)
shopid=shop_id// the shop_id is variable defined in the activity and it has the value retrieved from the database
}
}
}
Text(text =shopid)
}
That's not a good solution for 2 reasons:
the code will run at each recomposition, because you are using SideEffect, you probably want to use LaunchedEffect instead
placing your business logic in your composables is not the right solution, makes your composables tightly coupled to your business layer and hard to test
You should consider creating a ViewModel that will fetch the data from the database and then expose the value you want to display from the ViewMOdel using a MutableState object that you can then observe in your composable.
You can read this for more details.

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()
)
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)
}
}
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.

Jetpack Compose: Update composable when list changes

I've got a composable on a Screen which shows a list of Track items (favourites) :
var favourites: MutableList<Track> by mutableStateOf(mutableListOf())
#ExperimentalFoundationApi
#Composable
private fun ResultList(model: FreezerModel) {
with(model) {
if (favourites.isEmpty()) NoDataMessage("No favourites yet")
else {
LazyColumn(state = rememberLazyListState()) {
items(favourites) {
TrackCard(it, model)
}
}
}
}
}
On click events, I am updating my favourites list (add/remove item). How can I make my composable reflect these changes immediately (like to re-draw itself or something similar)? So far, it only works when I first switch to another screen.
Thanks for your inputs!
You need to use a MutableStateList<T> so that Compose can automatically recompose when the state changes.
From official doc:
Caution: Using mutable objects such as ArrayList<T> or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.
In your code use
val favourites = remember { mutableStateListOf<Track>()}
instead of
var favourites: MutableList<Track> by mutableStateOf(mutableListOf())
Just removing state = rememberLazyListState() from the lazyColumnFor params list should work in your case.
According to the doc if we use rememberLazyListState() then :-
Changes to the provided initial values will not result in the state being recreated or changed in any way if it has already been created.
After doing so , normally updating the list should work fine. Also its a good practice to expose an immutable list ( list ) instead of a mutableList to the composable.
For example use:-
var favourites by mutableStateOf(listOf<FavoriteItem>())
then add/remove/update the list by using :-
favourites = favourites + newList // add
favourites = favourites.toMutableList().also { it.remove(item) } // remove

Should livedata be always used in ViewModel?

It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}

Refreshing MutableLiveData of list of items

I'm using LiveData and ViewModel from the architecture components in my app.
I have a list of items that is paginated, I load more as the user scrolls down. The result of the query is stored in a
MutableLiveData<List<SearchResult>>
When I do the initial load and set the variable to a new list, it triggers a callback on the binding adapter that loads the data into the recyclerview.
However, when I load the 2nd page and I add the additional items to the list, the callback is not triggered. However, if I replace the list with a new list containing both the old and new items, the callback triggers.
Is it possible to have LiveData notify its observers when the backing list is updated, not only when the LiveData object is updated?
This does not work (ignoring the null checks):
val results = MutableLiveData<MutableList<SearchResult>>()
/* later */
results.value.addAll(newResults)
This works:
val results = MutableLiveData<MutableList<SearchResult>>()
/* later */
val list = mutableListOf<SearchResult>()
list.addAll(results.value)
list.addAll(newResults)
results.value = list
I think the extension is a bit nicer.
operator fun <T> MutableLiveData<ArrayList<T>>.plusAssign(values: List<T>) {
val value = this.value ?: arrayListOf()
value.addAll(values)
this.value = value
}
Usage:
list += anotherList;
According to MutableLiveData, you need to use postValue or setValue in order to trigger the observers.

Categories

Resources