Reputation: 431
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
)
...
}
Upvotes: 6
Views: 12200
Reputation: 41
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!!)
}
}
Upvotes: 0
Reputation: 2525
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:
progressList
immutableUpvotes: 4
Reputation: 4060
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.
Upvotes: 10
Reputation: 6825
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.
Upvotes: 4