mutableStateOf() not assinging new object? - android

I am updating the old mutableStateOf() object data by modifying it but new data is not reflecting on it.
variable: as
val offTime = mutableStateOf<List<OffTime>>(emptyList())
update code
fun updateOffTime(newOffTime: OffTime, index: Int){
val updatedOffTime = offTime.value.mapIndexed { i, offTime ->
var result = offTime
if(index == i) result = newOffTime
result
}
offTime.value = updatedOffTime
Log.d(TAG, "updateOffTime: $updatedOffTime")
Log.d(TAG, "updateOffTime: ${offTime.value}")
}
Note: It works when the object OffTime is without id
i.e. OffTime(fromTime, toTime) :- works
OffTime(id,fromTime, toTime) :- doesn't works

I don't know exactly your use-case, neither your entire code implementation, but if I may ask why are you using an ordinary collection list instead of a SnapshotStateList or an extension of its new instance, mutableStateListOf when its part of your requirement to perform list operations?
Have you tried converting your offtime as a SnapshotStateList like this?
val offTime = mutableStateListOf<OffTime>(mutableStateListOf())
and performing updates liks this?
offTime.add( ... )
//or
offtime.remove(...)
//or
offTime[index] = offtime copy
//or
var offTimeIterator = offTime.lisIterator() // where you can safely modify indeces
SnapshotStateList is created exactly for such use-cases in compose, where you can perform normal list operations such as (add, remove, update, or batch updates) and guarantees re-composition.
Your call
offTime.value = updatedOffTime // if this is a new instance of a list
will trigger an entire re-composition as the entire list reference had been changed, but with SnapshotStateList, any changes to the structure is guaranteed to match a specific re-composition, say if you modify an item at index 7, and if this is observed by say a LazyColumn only LazyColumn's 7th index will re-compose
Also I don't know if your Offtime is a data class or a standard class, I would recommend it to be a data-class so you can easily copy() a certain instance of it, pass a new value to a certain property of it and re-assign it in a SnapshotStateList.

Related

Combining two flows of paging data before showing in UI

I have this recycler view that shows paginated data with a PagingDataAdapter. It works well when making a single request to the service but I sometimes need to make two requests to get old and new events and show both in my recycler view. Is there a way I can combine the two flows of paginated data before showing them to the user? As it is, if I make the two requests, only the last one remains because of the invalidate() in the submitData method.
This is the current implementation that doesn't work the way I need it to:
private fun fetchEvents() {
fetchEventsJob?.cancel()
binding.cameraVmsEventsRecycler.adapter = null
fetchEventsJob = lifecycleScope.launch {
cameraViewModel.searchCameraEvents(
cameraId = videoDetector.id,
dateTo = if(eventsByDate) "" else dayPickerDateTo,
pageSize = REQUEST_LIMIT
cameraViewModel.filtersApplied
).collectLatest { eventsCollection ->
val events = eventsCollection.map { event ->
CameraEventModel.CameraEventItem(
VideoEventModel(
event.eventId,
event.faceId,
null,
event.state
)
)
}.insertSeparators {
before: CameraEventModel.CameraEventItem?, after: CameraEventModel.CameraEventItem? ->
renderSeparators(before, after)
}
binding.cameraEventsRecycler.adapter = eventsAdapter
eventsAdapter.submitData(events)
}
}
}
Upong calling fetchEvents() with different parameters, only the last flow of data remains due to the submitData().
Is there a way I can manage to do what I want? I can't use Room in this project.
You are explicitly calling collectLatest: it cancels collection when new items emit. The effect is, if you suspend in the collector lambda before your call to submitData() you only get the last item from the flow.
If you want to use all items from searchCameraEvents you might need to use toList():
val flow = cameraViewModel.searchCameraEvents(/*...*/)
val eventsCollection: List<PagingData<List<Event>>> = flow.toList()
val events = eventsCollection
// get list out of paging data
.flatMap { it.data }
// flatten list of lists
.flatten()
// map as needed
.map { event ->
CameraEventModel.CameraEventItem(/*...*/)
}
Mind, that you are handling paged data here. You want to make sure to keep a proper ordering. Also, if you retrieve more than one page from your flow, you're actually skipping the paging mechanism somehow. So you might also need to keep track of this in order not to produce duplicates in the UI.

correct way to handle mutable state of list of data in jetpack compose

For a mutable state containing integer like this,
var data by rememberSaveable {
mutableStateOf(-1)
}
We can update data using
data = 5
And it would update the data and trigger recomposition.
Now, my requirement is to remember a list of integers.
Declared like this,
var data by rememberSaveable {
mutableStateOf(mutableListOf<Int>())
}
The list operations like clear(), add(), and remove() update the data in the list but recomposition is not triggered as the list instance is still the same.
To handle this, I replaced the following list operations with these assignments.
Instead of data.clear(), used data = arrayListOf()
Instead of data.add(ele), used data = mutableListOf(*data.toTypedArray(), ele)
Similarly, for remove() tried this,
data.remove(ele)
data = mutableListOf(*data.toTypedArray())
But this is not triggering recomposition.
What is the correct alternative for remove()?
And is there any generic to handle all list operations?
You have two options here:
Declare your list as mutableStateListOf. In this case you can do any operations on it, like clear(), add(), remove(). See this answer how you can use it with rememberSaveable, or move it to the view model.
val data = remember {
mutableStateListOf<Int>()
}
data.clear()
Sometimes it's more comfortable to use mutableStateOf, in this case you can update it value like this:
var data by rememberSaveable(Unit) {
mutableStateOf(listOf<Int>())
}
data = data.toMutableList().apply {
clear()
}.toImmutableList()

How to change visibility of a TextView if a MutableList is empty? (Android/Kotlin)

I'm working on a simple calorie counter app using two fragments and a ViewModel. I'm a beginner and this is a modification of an app I just created for a course (this app is not a homework assignment). It uses ViewModel and has a fragment that collects user input and a fragment that displays the input as a MutableList of MutableLiveData. I would like for the list screen to initially be empty except for a TextView with instructions, and I'd like the instructions to disappear once an entry has been added to the list. My class instructor told me to use an if-else statement in the fragment with the list to achieve this, but it's not working. He didn't tell me exactly where to put it. I tried a bunch of different spots but none of them worked. I don't get errors - just no change to the visibility of the TextView.
Here is the code for the ViewModel with the list:
val entryList: MutableLiveData<MutableList<Entry>>
get() = _entryList
init {
_entry = MutableLiveData<Entry>()
_entryList.value = mutableListOf()
}
fun addEntry(entryInfo: Entry){
_entry.value = entryInfo
_entryList.value?.add(_entry.value!!)
}
}
And this is the code for the observer in the list fragment:
Observer { entryList ->
val entryListView: View = inflater.inflate(R.layout.fragment_entry_list, null, false)
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.VISIBLE
} else {
entryListView.instructions_text_view.visibility = View.GONE
}
entryList.forEach {entry ->
val view: View = inflater.inflate(R.layout.entry_list_item, null, false)
view.date_entry_text_view.text = String.format(getString(R.string.date), entry.date)
view.calories_entry_text_view.text =
view.line_divider
binding.entryList.addView(view)
}
Thanks for any help.
I guess you are expecting your observer to get notified of the event when you are adding entryInfo to your event list (_entryList.value?.add(_entry.value!!).
But this won't happen as you are just adding an element to the same mutable list, and as the list reference hasn't changed, live data won't emit any update.
To solve this, you have two options.
Create a new boolean live data which controls when to show and hide the info text. Set its initial value to false, and update it to true in addEntry() function.
Instead of updating the same mutable list, create of copy of it, add the element and set the entryList.value equal to this new list. This way your observer will be notified of the new list.
Additionally, its generally not a good practice to expose mutable data unless there is no alternative. Here you are exposing a mutable list of Entry and that too in the form of a mutable live data. Ideally, your should be exposing LiveData<List<Entry>>.
This is one possible implementation of all the points that I mentioned:
private val _entryList = MutableLiveData(listOf<Entry>()) // Create a private mutable live data holding an empty entry list, to avoid the initial null value.
val entryList: LiveData<List<Entry>> = _entryList // Expose an immutable version of _entryList
fun addEntry(entryInfo: Entry) {
_entryList.value = entryList.value!! + entryInfo
}
I haven't used the _entry live data here, but you can implement it the same way.
set your viewModel to observe on entry added.
I think you have gotten your visibility toggle in the your if else blocks wrong.
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.GONE // OR View.INVISIBLE
} else {
entryListView.instructions_text_view.visibility = View.VISIBLE
}
Your Observer should get notified of changes to entryList when _entryList has changed. Make sure you are calling addEntry() function to trigger the notification.

Jetpack Compose MutableLiveData not updating UI Components

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

Changing value of a LiveData Calendar do not trigger Observer

In the fragement I display a Date from a Calendar object. This date is got from the viewModel of the fragement and it's a live date :
private val _theDate = MutableLiveData<Calendar>()
val theDate: LiveData<Calendar>
get() = _theDate
In the viewModel also I have a function that add 1 day to _theDate
fun goToDayAfter() {
_theDate.value!!.add(Calendar.DATE, 1)
}
This function is called after clicking on a button of the same fragment, and it does not trigger the observer :
viewModel.theDate.observe(viewLifecycleOwner, androidx.lifecycle.Observer { newDate ->
displayDate(newDate)
})
For more details and after debugging, I believe that _theDate is well changed but the observer is not triggered, If I change to another fragement and come back the new _theDate is changed.
Also if I change the method goToDayAfter() to :
fun goToDayAfter() {
val tmp = _theDate.value!!
tmp.add(Calendar.DATE, 1)
_theDate.value = tmp
}
It works!
Why cahnging _theDate directly do not trigger the observer ? is it because it s an object and not a premitive ? Is there any better solution that to pass by a tmp variable ?
For the update to trigger, you have to set the value of the LiveData, like you have discovered, i.e. by using:
_theDate.value = ...
Othwerwise, if you mutate (i.e. change) the value that the LiveData points to (_theDate.value!!.add(Calendar.DATE, 1), it has no way of knowing that the value was changed! That's why no update is triggered.
Prefer immutable objects
I recommend using immutable objects and re-creating them every time you want to change them. With immutable objects it's more difficult to run into this kind of bugs. An added bonus is that the code that observes _theDate cannot change it.
So instead of Calendar, I would use java.time.LocalDateTime. You can use it if you add a dependency to desugar_jdk_libs in your build.gradle file:
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
in which case your code would like like this:
fun goToDayAfter() {
_theDate.value = _theValue!!.plusDays(1)
}

Categories

Resources