room LiveData how to ignore some properties change - android

I'm using room query and return LiveData to display elements on UI. The problem is the entity changes most of the properties very often which isn't relative to the UI and due to the UI refreshed many times brings no good.
What I want is like swift combine #Published.
Here is the code:
#Entity
#Parcelize
data class Foo(#PrimaryKey var code: String,
var p1: Double,
var p2: Int? = null,
var p3: Int? = null,
var p4: Double? = null,
var p5: Int? = null,
var p6: Double? = null,
var p7: Int? = null
): Parcelable
Actually I only care about code property changes insert/delete.
#Query("SELECT * FROM Foo WHERE code IN (:fooIds)")
fun getLiveDataListBy(fooIds`: List<String?>): LiveData<List<Foo>?>?
I have the property in ViewModel and observe it in fragment.
var foosLiveData: LiveData<List<Fool>>? = null
viewModel.foosLiveData?.observe(viewLifecycleOwner, {
adapter.foos = it
adapter.notifyDataSetChanged()
})
The p1 to p7 properties are keep changing. Due to the list refresh all the time.
Right now, I could improve it by checking
if (adapter.foos != it) {
adapter.foos = it
adapter.notifyDataSetChanged()
}
But this almost no improvement.
then if might be improved by this: (I haven't tested)
adapter.foos = it
adapter.notifyDataSetChanged()
}
this could be works, but it will keep check the map, only might take adapter.foos.map { a -> a.code } out to save a bit.
This might another workaround.
I also thought take the code out and use a new variable var codeObserver: MutableLiveData(List<String>) = MutableLiveData()
Then
viewModel.foosLiveData?.observe(viewLifecycleOwner, {
viewModel.codeObserver.value = it.foo.map { it.code}
})
viewModel.codeObserver.observe(viewLifecycleOwner, {
adapter.foos = viewModel.foosLiveData?.value
adapter.notifyDataSetChanged()
}
Well, I haven't test the above code, but looks like not right direction.
So any better or right way to achieve observe only one or few properties?

You can avoid reloading the recycler view adapter by using AsyncListDiffer. Refer to Google's Documentation for further information. In simple words, it can consume the values from a LiveData of List and present the data simply for an adapter. It computes differences in list contents via DiffUtil on a background thread as new Lists are received.

Related

Kotlin - StateFlow not emitting updates to its collectors

I got a StateFlow of type UserStateModel (data class) in my app.
private val _userStateFlow: MutableStateFlow<UserStateModel?> = MutableStateFlow(UserStateModel())
val userStateFlow: StateFlow<UserStateModel?> = _userStateFlow
here is the UserStateModel
data class UserStateModel(
val uid: String? = null,
val username: String? = null,
val profileImageUrl: String? = null,
var isLoggedIn: Boolean = false,
val isPremiumUser: Boolean = false,
val posts: List<Post>? = listOf()
)
When I update the StateFlow with a new Username it emits the change to the collectors and the UI updates.
But when I change a property inside the posts: List? list it doesnt emit the changes.
When I change the size of the list it does, when I change the name property of the Post at index 0 it doesnt.
How can I detect changes to the child properties of the Data class?
Right now I use an ugly workaround, I add
val updateErrorWorkaround: Int = 0
to the UserStateModel data class and increase it by one so the collectors get notified
P.s I'm using MVVM + Clean Architecture and Jeptack Compose
EDIT
Thats my Post Model:
data class Post(
val id: Int,
val name: String,
val tags: MutableList<Tag>? = null
)
Here is how I update the MutableList:
val posts = userStateFlow.value?.posts
posts.get(index).tags?.add(myNewTag)
_userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Those changes are not emitted to the collectors
StateFlow emits only if it detects changes to the value, it ignores replacing the value with the same data. To do this, it compares the previous value with the new one. For this reason, we shouldn't modify the data that we already provided to the StateFlow, because it won't be able to detect changes.
For example, we set value to a User(name=John). Then we mutate the same user object by modifying its name to James and we set the value to this "new" user object. StateFlow compares "new" User(name=James) with its stored value, which is now also User(name=James), so it doesn't see any changes.
In your example you created a copy of UserStateModel, but inside you re-use the same objects and you mutate them. In this case you added a new item to tags and this change affected old UserStateModel as well, so StateFlow doesn't detect the change.
To fix the problem, you need to copy all the data that was changed and do not mutate anything in-place. It is safer to make all the data immutable, so val and List - this way you are forced to make copies. I changed tags to val tags: List<Tag> = listOf(), then your code could look like the following:
val posts = userStateFlow.value?.posts!!.toMutableList()
posts[index] = posts[index].copy(tags = posts[index].tags + myNewTag)
userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Here we create a copy of not only UserStateModel. We also copy posts list, the Post that we modify and we also copy the list of tags.
Alternatively, if this behavior of StateFlow is more annoying to you than helpful, you can use SharedFlow which doesn't compare values, but just emits.

How can DiffUtil know about list changes when only the companion object has changed?

As shown in the image, I would like the unit of the Detail item to be changed at once according to the toggle button.
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
However, it seems that DiffUtil determines that there is no change between the new list and the old list, perhaps because the unit property is set as a companion object.
So there is no update of the view either.
How can I make DiffUtil responsive while changing the companion object?
Detail
#Entity
data class Detail(
#PrimaryKey(autoGenerate = true)
var id: Int,
val set: Int,
var weight: String = "",
var reps: String = "") {
companion object {
var title: String = ""
var unit: String = "kg"
val memo = ""
}
}
ViewModel
class DetailViewModel(application: Application) : ViewModel() {
private val repository: DetailRepository
private val _items: MutableLiveData<List<Detail>> = MutableLiveData()
val items = _items
private val list: List<Detail>
get() = _items.value ?: emptyList()
init {
val detailDao = DetailDatabase.getDatabase(application)!!.detailDao()
repository = DetailRepository(detailDao)
}
fun changeUnit(unit: String) {
Detail.unit = unit
if(list == null)
return
_items.postValue(list) // To notify the observer.
}
fun addDetail() {
viewModelScope.launch(Dispatchers.IO){
val item = Detail(0, set = list.size+1)
repository.add(item)
// If use plus(), a new List is returned.
// Therefore, the change is notified to the Observer by postValue of the new list added.
_items.postValue(list.plus(item))
}
}
fun deleteDetail() {
// Delete the last set and return a new list to postValue to notify the Observer of the change.
_items.postValue(list.dropLast(1))
}
}
DiffUtil
class DetailDiffCallback : DiffUtil.ItemCallback<Detail>() {
override fun areItemsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return oldItem == newItem
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.items.observe(viewLifecycleOwner) { newList ->
adapter.submitList(newList)
}
}
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
This is the root of the problem. If you want DiffUtil to be able to "see" these changes, you will have to move this information out of the companion object.
DiffUtil works by taking in two instances of your class and doing work (the areItemsTheSame() and areContentsTheSame() methods) to see if anything has changed. Since this information is part of the companion object, it will always be identical for all instances, which means there's no way for DiffUtil to detect a change, even if one has happened.
Like Ben P says, when you change the value in the companion object, that affects the entire class (since they all share that object). It's not included in the generated equals() code for the data class - because why would it need to be? Every instance shares the same value, it's not part of the state!
Even if it were, when you compare oldList and newList, oldList has still been "updated" with the new unit value, because it's in that shared companion object. If you want oldList and newList to be able to have different values for unit, they need to be per-instance properties.
But the way you're doing things here, the unit doesn't even seem to be part of the data, right? It's not like you're storing a value and a unit of measure, and doing conversions when the displayed unit type changes. It looks like this is just a general display option, that just applies to how the data is displayed.
So in that case, why not just call notifyDataSetChanged() on the adapter, or something similar? Force a redraw, let it show the new unit type, that's it. I'm not sure if you need to do anything special if you're using DiffUtil, but that's what I'd look into.
(I feel like storing the unit type in the data would be a way better approach, seems important to what the stored number actually means, but for what you're doing right now, a display refresh should be enough)

DiffUtil not refreshing view in Observer call android kotlin

Hey I am using diff util with ListAdapter. The updating of list works but I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged(). I tried everything inside this answer ListAdapter not updating item in RecyclerView only working for me is notifyItemChanged or setting adapter again. I am adding some code. Please someone know how to fix this problem?
Data and Enum class
data class GroupKey(
val type: Type,
val abc: Abc? = null,
val closeAt: String? = null
)
data class Group(
val key: GroupKey,
val value: MutableList<Item?> = ArrayDeque()
)
enum class Type{
ONE,
TWO
}
data class Abc(
val qq: String? = null,
val bb: String? = null,
val rr: RType? = null,
val id: String? = null
)
data class RType(
val id: String? = null,
val name: String? = null
)
data class Item(
val text: String? = null,
var abc: Abc? = null,
val rr: rType? = null,
val id: String? = null
)
viewmodel.kt
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData(ArrayDeque())
fun populateList(){
// logic to call api
list.postValue(data)
}
fun addItemTop(){
// logic to add item on top
list.postValue(data)
}
inside view model I am filling data by api call inside viewmodel function and return value to list. Also another function which item is inserting at top of list so that's why is used ArrayDeque
Now I am adding nested reyclerview diff util callback.
FirstAdapter.kt
class FirstAdapter :
ListAdapter<Group, RecyclerView.ViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Group>() {
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean {
return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
}
}
}
......... more function of adapter
}
FirstViewHolder
val adapter = SecondAdapter()
binding.recyclerView.adapter = adapter
adapter.submitList(item.value)
SecondAdapter.kt
class SecondAdapter : ListAdapter<Item, OutgoingMessagesViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return ((oldItem.rr == newItem.rr) &&
(oldItem.text == oldItem.text) && (oldItem.abc == newItem.abc))
}
}
}
..... more function
}
Activity.kt
viewModel.list.observe(this, { value ->
submitList(value)
})
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list)
// adapter?.notifyDataSetChanged()
}
I am 100% sure that my list is updating and my observer is calling when my new list is added. I debug that through debug view. But problem is I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged()
UPDATE
viewmodel.kt
class viewModel : BaseViewModel(){
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData()
//... more variables...
fun fetchData(context: Context) {
viewModelScope.launch {
val response = retroitApiCall()
response.handleResult(
onSuccess = { response ->
list.postValue(GroupData(response?.items, context))
},
onError = { error ->
Log.e("error" ,"$error")
}
)
}
}
}
internal fun GroupData(items: List<CItem>?, context: Context): MutableList<Group> {
val result: MutableList<Group> = MutableList()
items?.iterator()?.forEach { item ->
// adding item in list by add function and then return list.
return result
}
private fun addItemOnTop(text: String) {
list.value?.let { oldlist ->
// logic to add items on top of oldlist variable
if(top != null){
oldlist.add(0,item)
}else{
val firstGroup = oldlist[0]
firstGroup.value.add(item)
}
list.postValue(oldlist)
}
}
}
I am using sealed class something like this but not this one Example. And Something similar to these when call api Retrofit Example. Both link I am giving you example. What I am using in my viewmodel.
I don't know what's going on, but I can tell you two things that caught my attention.
First Adapter:
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
You're not comparing if the items are the same, you're comparing the items and their contents are the same. Don't you have an Id like you did in your second adapter?
I'd probably check oldItem.key == newItem.key.
Submitting the List
As indicated in the answer you linked, submitList has a very strange logic where it compares if the reference of the actual list is the same, and if it is, it does nothing.
In your question, you didn't show where the list comes from (it's observed through what appears to be liveData or RXJava), but the souce of where the list is constructed is not visible.
In other words:
// P S E U D O C O D E
val item1 = ...
val item2 = ...
val list1 = mutableListOf(item1, item2)
adapter.submitList(list1) // works fine
item1.xxx = ""
adapter.submitList(list1) // doesn't work well.
WHY?
Unfortunately, submitList's source code shows us that if the reference to the list is the same, the diff is not calculated. This is really not on the adapter, but rather on AsyncListDiffer, used by ListAdapter internally. It is this differ's responsibility to trigger the calculation(s). But if the list references are the same, it doesn't, and it silently ignores it.
My suspicion is that you're not creating a new list. This rather undocumented and silent behavior hurts more than it helps, because more often than not, developers aren't expecting to duplicate a list supplied to an object whose purpose and promise is to offer the ability to "magically" (and more importantly, automatically) calculate its differences between the previous.
I understand why they did it, but I would have at the very least emitted a log WARNING, indicating you're supplying the same list. Or, if you want to avoid polluting the already polluted logCat, then at least be much more explicit about it in its official documentation.
The only hint is this simple phrase:
you can use submitList(List) when new lists are available.
The key here being the word new lists. So not the same list with new items, but simply a new List reference (regardless of whether the items are the same or not).
What should you try?
I'd start by modifying your submitList method:
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list.toMutableList())
}
For Java users out there:
adapter.submitList(new ArrayList(oldList));
The change is to create a copy of the list you receive: list.ToMutableList(). This way the AsyncListDiffer's check for list equality will return false and the code will continue.
UPDATE / DEBUG
Unfortunately, I don't know what is going on with your code; I assure you that ListAdapter works, as I use it myself on a daily basis; If you think you've found a case where there are problems with it, I suggest you create a small prototype and publish it on github or similar so we can reproduce it.
I would start by using debug/breakpoints in key areas:
ViewModel; write down the reference fromthe list you "return".
DiffUtil methods, is diffUtil being called?
Your submitList() method, is the list reference the same as the one you had in your ViewModel?
etc.
You need to dig a bit deeper until you find out who is not doing what.
On Deep vs Shallow copy and Java and whatever...
Please keep in mind, ListAdapter (through AsyncDiff) checks if the reference to the list is the same. In other words, if you have a list val x = mutableListOf(...) and you give this to the adapter, it will work the 1st time.
If you then modify the list...
val x = mutableListOf(...)
adapter.submitList(x)
x.clear()
adapter.submitList(x)
This will NOT WORK correctly, because to the eyes of the Adapter both lists are the same (they actually are the same list).
The fact that the list is mutable is irrelevant. (I still frown upon the mutable list; why does submitList accept a mutable list if you cannot mutate it and submit it again, escapes my knowledge but I would not have approved that Pull Request like so) It would have avoided most problems if they only took a non-mutable list, therefore implying you must supply a new list every time if you mutate it. Anyway...
as I was saying, duplicating a list is simple, in either Kotlin or Java there are multiple variations:
val newListWithSameContents = list1.toList()
List newListWithSameContents = ArrayList(list1);
now if list1 has an item...
list1.add("hello")
When you copy list1 into newList... The reference to "Hello" (the string) is the same. If String were mutable (it's not, but assume it is), and you modified that string somehow... you would be modifying both strings at the same time or rather, the same string, referenced in both lists.
data class Thing(var id: Int)
val thing = Thing(1)
val list1: MutableList<Thing> = mutableListOf(thing)
val list2: MutableList<Thing> = list1.toMutableList()
println(list1)
println(list2)
// This prints
[Thing(id=1)]
[Thing(id=1)]
Now modify the thing...
thing.id = 2
println(list1)
println(list2)
As expected, both lists, pointing to the same object:
[Thing(id=2)]
[Thing(id=2)]
This was a shallow copy because the items were not copied. They still point to the same thing in memory.
ListAdapter/DiffUtil do not care if the objects are the same in that regard (depending how you implemented your diffutil that is); but they certainly care if the lists are the same. As in the above example.
I hope this clarifies what is needed for ListAdapter to dispatch updates. If it fails to do so, then check if you're effectively doing the right thing.

Problem with filtering Mutablelist of type class

So what I'm trying to do is to write search logic. The problem is following filter does not work even tho I do have an element containing following letter. So what my question is why is it not returning the expected value and if I'm doing something wrong what is it.
the filter I'm trying to use:
model.data.filter { person -> person.employeeName.toLowerCase().contains("t")}.toMutableList()
where model is InfoModel type and InfoModel looks like this:
class InfoModel {
var status = ""
lateinit var data : MutableList<Data>
class Data {
var id = ""
#SerializedName("employee_name")
var employeeName = ""
#SerializedName("employee_salary")
var employeeSalary = ""
#SerializedName("employee_age")
var employeeAge = ""
#SerializedName("profile_image ")
var profileImage = "https://www.pngitem.com/pimgs/m/146-1468479_my-profile-icon-blank-profile-picture-circle-hd.png"
}
}
I'm guessing due to lack of context, but maybe you're doing something like this:
model.data.filter { person -> person.employeeName.toLowerCase().contains("t")}.toMutableList()
println(model.data) // Still prints original unfiltered list!
The first line of code creates a new MutableList and promptly throws it away, because you don't assign it to anything. So the original list pointed at by model.data is left unchanged.
Since data is a MutableList, you can modify it in place using retainAll:
model.data.retainAll { person -> person.employeeName.toLowerCase().contains("t") }
Alternatively, you could reassign the result of your original code back to model.data:
model.data = model.data.filter { person -> person.employeeName.toLowerCase().contains("t")}.toMutableList()
To me it looks like kind of code smell to have a MutableList assigned to a read-write var, because then it's mutable in two different ways. Why does it even have to be lateinit if it's mutable? You could instantiate with an empty list and fill it later.
In general var data: List should be preferred to val data: MutableList unless you are needing to optimize performance for huge lists. And var data: MutableList is just inviting troubles.

PagedListAdapter.submitList() Behaving Weird When Updating Existing Items

Little story of this topic : the app just updating clicked row's values with dialog when confirmed. Uses pagination scenario on room database.
When an item added or removed, the latest dataset is fetched and passed to submitList method, then all changes are seen and worked well.
The problem starts there, if an existing item updated, again the latest dataset is fetched properly and passed to submitList, but this time changes didn't seem.
When i debug the DIFF_CALLBACK and caught my item in areItemsTheSame, the newHistory and oldHistory values are same! (How!)
There could be any bug in submitList method ?
Room v. : 2.1.0-alpha02
Paging v. : 2.1.0-beta01
After initializing, observe fetches list from room and passes to mHistoryAdapter.submitList(it). Then if i update an item, observe gets triggered again(and i'm seeing updated value in param it) and passes to submitList.
Unfortunately, adapter wont change...
mResolvedAddressViewModel = ViewModelProviders.of(this).get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
All the parts
Model
#Parcelize
#Entity
data class ResolvedAddress(
#PrimaryKey var id: String = UUID.randomUUID().toString(),
var requestedLat: Double = 0.0,
var requestedLon: Double = 0.0,
var requestedAddress: String = "",
var lat: Double,
var lon: Double,
var address: String,
var country: String,
var countryCode: String,
var city: String,
var alias: String? = null,
var favorite: Boolean = false,
var provider: String? = null,
var lastUseDate: Long = 0L) : Parcelable
Adapter
class HistoryAdapter(var context: Context)
: PagedListAdapter<ResolvedAddress, HistoryItemHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ResolvedAddress>() {
override fun areItemsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory.id == newHistory.id
}
override fun areContentsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory == newHistory
}
}
}
}
Fragment
class HistoryFragment : Fragment() {
private lateinit var mHistoryAdapter: HistoryAdapter
private lateinit var mResolvedAddressViewModel: ResolvedAddressViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerViewHistory.setHasFixedSize(true)
recyclerViewHistory.layoutManager = LinearLayoutManager(activity)
recyclerViewHistory.itemAnimator = DefaultItemAnimator()
mHistoryAdapter = HistoryAdapter(context!!)
recyclerViewHistory.adapter = mHistoryAdapter
mResolvedAddressViewModel = ViewModelProviders.of(this)
.get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
}
}
There's a couple things missing from the question that could help provide a more detailed answer.
Ex. What does your RecyclerView.Adapter look like? Does it extend PagedListAdapter?
What does your model class look like? Is it a Kotlin data class?
For the sake of providing an answer, let's assume those unknowns are what we expect.
If I understand the question, it seems like you're just updating an item and not removing or adding any items.
Therefore, the DiffUtil.ItemCallback's areItemsTheSame will always return true, because the old list and new list has not been modified in terms of their size.
Meaning, if you've updated an item, you've probably updated it's contents and not removed it from the list.
Therefore, areItemsTheSame will return true, because their ids are still the same.
It's more likely that the second method, areContentsTheSame will return false since you've updated the item's content.
If your model class, ResolvedAddress, is a Kotlin data class, then the method areContentsTheSame should return false when comparing the item that was updated from the old list and the new list. This should trigger the onBindViewHolder method in your adapter at this point for you to rebind that item with the updated data.
If that model is not a Kotlin data class, than you must make sure the class implements the compareTo method. If not, you are comparing the object's memory address vs the actual contents of the object. If that is the case, the method areContentsTheSame will always return true, since the object's memory address has not changed.
These are some debugging tips, as it is difficult to provide a clearer answer without more knowledge about how the code has been implemented.
I was having a similar issue but managed to fix it by updating the existing item with a new object rather than directly updating the existing item, as suggested by this answer here:
https://stackoverflow.com/a/54505078/10923311
The issue is with how submitList processes changes. If you are passing a reference to the same list, it will not show updates as it determines it is the same datasource. In Kotlin if you want to update the sourceList and pass it back to submitList, you can do so as follows:
submitList(originalList.toList().toMutableList().let {
it[index] = it[index].copy(property = newvalue) // To update a property on an item
it.add(newItem) // To add a new item
it.removeAt[index] // To remove an item
// and so on....
it
})

Categories

Resources