5

I am trying to display several download progress bars at once via a list of data objects containing the download ID and the progress value. The values of this list of objects is being updated fine (shown via logging) but the UI components WILL NOT update after their initial value change from null to the first progress value. Please help!

I see there are similar questions to this, but their solutions are not working for me, including attaching an observer.

class DownLoadViewModel() : ViewModel() {
   ...
   private var _progressList = MutableLiveData<MutableList<DownloadObject>>()
   val progressList = _progressList // Exposed to the UI.
   ...
   
   //Update download progress values during download, this is called 
   // every time the progress updates.
   val temp = _progressList.value
   temp?.forEach { item ->
      if (item.id.equals(download.id)) item.progress = download.progress
   }
   _progressList.postValue(temp)
   ...
}

UI Component

@Composable
fun ExampleComposable(downloadViewModel: DownloadViewModel) {
    val progressList by courseViewModel.progressList.observeAsState()
    val currentProgress = progressList.find { item -> item.id == local.id }
    ...
    LinearProgressIndicator(
        progress = currentProgress.progress
    )
    ...
}
Nicolas Mage
  • 291
  • 1
  • 4
  • 19

4 Answers4

10

I searched a lot of text to solve the problem that List in ViewModel does not update Composable. I tried three ways to no avail, such as: LiveData, MutableLiveData, mutableStateListOf, MutableStateFlow

According to the test, I found that the value has changed, but the interface is not updated. The document says that the page will only be updated when the value of State changes. The fundamental problem is the data problem. If it is not updated, it means that State has not monitored the data update.

The above methods are effective for adding and deleting, but the alone update does not work, because I update the element in T, but the object has not changed.

The solution is to deep copy.


    fun agreeGreet(greet: Greet) {
        val g = greet.copy(agree = true)  // This way is invalid 
        favourites[0] = g
    }


    fun agreeGreet(greet: Greet) {
        val g = greet.copy() // This way works
        g.agree = true
        favourites[0] = g
    }

Very weird, wasted a lot of time, I hope it will be helpful to those who need to update.

gaohomway
  • 2,132
  • 1
  • 20
  • 37
  • yes thank you for updating this question, it has been answered on another page but very helpful to have the answer here as well. – Nicolas Mage Sep 16 '21 at 17:30
  • here is a link to the other, very similar, question with a working solution https://stackoverflow.com/questions/68781116/android-update-activity-with-value-of-data-object-field-from-mutablestateflowli/68783789?noredirect=1#comment121562333_68783789 – Nicolas Mage Sep 16 '21 at 17:33
  • 1
    It’s a pity that the most-viewed official document will not prompt these. Fortunately in regret, we meet here – gaohomway Sep 17 '21 at 05:36
4

It is completely fine to work with LiveData/Flow together with Jetpack Compose. In fact, they are explicitly named in the docs.

Those same docs also describe your error a few lines below in the red box:

Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.

Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.

Instead of using non-observable mutable objects, we recommend you use an observable data holder such as State<List> and the immutable listOf().

So the solution is very simple:

  • make your progressList immutable
  • while updating create a new list, which is a copy of the old, but with your new progress values
m.reiter
  • 1,796
  • 2
  • 11
  • 31
3

As far as possible, consider using mutableStateOf(...) in JC instead of LiveData and Flow. So, inside your viewmodel,

class DownLoadViewModel() : ViewModel() {
   ...
   private var progressList by mutableStateOf(listOf<DownloadObject>()) //Using an immutable list is recommended
   ...
   
   //Update download progress values during download, this is called 
   // every time the progress updates.
   val temp = progress.value
   temp?.forEach { item ->
      if (item.id.equals(download.id)) item.progress = download.progress
   }
   progress.postValue(temp)
   ...
}

Now, if you wish to add an element to the progressList, you could do something like:-

progressList = progressList + listOf(/*item*/)

In your activity,

@Composable
fun ExampleComposable(downloadViewModel: DownloadViewModel) {
    val progressList by courseViewModel.progressList
    val currentProgress = progressList.find { item -> item.id == local.id }
    ...
    LinearProgressIndicator(
        progress = currentProgress.progress
    )
    ...
}

EDIT,

For the specific use case, you can also use mutableStateListOf(...)instead of mutableStateOf(...). This allows for easy modification and addition of items to the list. It means you can just use it like a regular List and it will work just fine, triggering recompositions upon modification, for the Composables reading it.

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
  • 1
    Hey consider taking the compose codelabs by the way. There's one for state too. – Richard Onslow Roper Aug 05 '21 at 19:47
  • Really help you grasp the concepts – Richard Onslow Roper Aug 05 '21 at 19:47
  • So how would I modify the current contents of the list in a way the UI could observe it? Your example uses a progressList = progressList + listOf(/*item*/) approach which would add items to the list instead of modifying existing items (using a ton of memory). – Nicolas Mage Aug 05 '21 at 21:19
  • 1
    For that, you could use, `progressList = progressList.toMutableList().also { it[index] = newItem }` – Richard Onslow Roper Aug 06 '21 at 06:36
  • Update: Yes I've already taken the state code lab and I'm following their documented implementation. Your code actually has several errors in it and won't even compile but thanks for the help. You cannot observe 'mutableStateOf' in an activity the way you can with MutableLiveData. Still not sure why my UI won't update with the correct data, i'm logging the exposed public variable and the value is being update. The UI just isn't observing the update for some reason /boggle. – Nicolas Mage Aug 10 '21 at 18:54
  • 1
    Well, a) With `MutableState` object types, you do not need to "observe", Compose is pre-configured to observe any changes to the value, b) I wrote that code in the answer box itself, on a mobile device; didn't know it had to compile, since I was just trying to give you the core idea, i.e., using `MutableState` objects instead of `LiveData` and `Flow`, and c) Check out the docs, they clearly mention, in fact don't check out the docs - You said that you took the codelab right? Over there they 'replace' `LiveData` with `MutableState`, while demonstrating it (as of course, a best practice) – Richard Onslow Roper Aug 10 '21 at 19:06
  • 1
    You are not alone who is facing issues. Check the site for similar questions, and you'll find at least three (that I have answered myself) people suffering from the same issue, whose problem got fixed by swapping it out with `MutableState`. One is as recent as yesterday (or today, depending upon your time zone) – Richard Onslow Roper Aug 10 '21 at 19:07
  • What is the issue you are facing? I mean if you are bent on proving `LiveData` better suited for Compose as compared to `MutableState`, then I'm sorry, you're not gonna succeed at that. Tell me the problem, maybe I'll be able to debug it. Regards, – Richard Onslow Roper Aug 10 '21 at 19:08
  • Thanks a lot for the continued effort to help! The following approach you supplied works to add/replace an element of the progressList `progressList = progressList.toMutableList().also { it[0] = DownloadObject }` but when I change the value of an element of that object like this, the value is updated via logging but the UI does not observe the data change `progressList = progressList.toMutableList().onEach { downloadObject -> if (downloadObject.id == requestObject.id) downloadObject.progress = downloaded / total }` with multiple items in the progressList at a time, index isn't a good access – Nicolas Mage Aug 10 '21 at 21:19
  • Yeah, I mentioned in the edit of my answer telling that now you can use it as a regular List variable, if you are using `mutableStateListOf`. The approach you repeated in your comment was actually meant for pre-edit code, where you used `mutableStateOf(listOf())`. At the time I took the codelab, this was the recommended way. It has been updated since. – Richard Onslow Roper Aug 10 '21 at 22:17
  • After creating a `mutableStateListOf` object, you can use its pre-defined methods like `list.add(...)`, and all. – Richard Onslow Roper Aug 10 '21 at 22:18
  • Your edit suggestion still isn't reliably recomposing the UI. Frustrating.The viewModel list = `var progressList = mutableStateListOf()`, viewmodel update download prgress logic = `progressList = progressList.onEach { item -> if (item.id == id) { item.progress = progress } }`, UI variable = `val progressList = courseViewModel.progressList`. I've been stuck on this a while now. Thanks for the help. – Nicolas Mage Aug 13 '21 at 23:51
  • What is `CourseResourceDownloadRequest`? Actually you cannot treat any object type as `MutableState`. Primitive types like String, and all are fine. Also, the immutable `listOf()` is also fine. Was the first approach triggering recompositions fine? – Richard Onslow Roper Aug 14 '21 at 09:11
  • This worked for me. Swapping mutableStateListOf instead of MutableLiveData and then observing as state in the composable. But, my question is, for managing states should we not use LiveData? – Anmol Singh Sahi Jun 17 '22 at 02:18
0

Yes, I know it's not the best implementation, however for me personally it helped:

Fragment:

class Fragment : Fragment() {

   private val viewModel : MyViewModel by viewModels()
   private val flow by lazy {
      callbackFlow<List<Item>> {
         viewModel.viewModelItmes.observe(viewLifecycleOwner) {
            trySend(it)
         }
         awaitClose()
      }
   }
   
   ...

   @Composable private fun Root() {
      val items = remember { mutableStateListOf<ChatListItem>() }
      LaunchedEffect(Unit) {
         flow.collect {
            items.clear()
            items.addAll(it)
         }
      }
      Column {
         items.forEach { /* ... */ }
      }
   }
}

ViewModel:

class MyViewModel : ViewModel() {

   val viewModelItmes = MutableLiveData<ArrayList<Item>>(emptyList())
   
   fun someOneAddItem(item : Item) {
      viewModelItmes.value!!.add(item)
      viewModelItmes.postValue(viewModelItmes.value!!)
   }

}