Kotlin - StateFlow not emitting updates to its collectors - android

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.

Related

Android mutableStateOf() value property not changing

I have a data class.
data class ServiceInfoState(
val availableServices : List<Services> = emptyList(),
val loadingState : AvailableServicesState = AvailableServicesState.Loading
)
In my view model I keep the state as such:
private val _uiState = mutableStateOf(ServiceInfoState())
val uiState : State<ServiceInfoState> = _uiState
Later I create a new updated list of services and want to update the state.
_uiState.value = uiState.value.copy(availableServices = updated)
It is my understanding that this will create a new shallow copy of uiState.value, update the available services list, and assign this to the private property _uiState.value. This change to the value property should cause the UI which is watching the publicly exposed version uiState to recompose. This does not happen.
I then check the code like this:
println("List Equality: ${_uiState.value.availableServices === updated}")
val before = _uiState.value
_uiState.value = uiState.value.copy(availableServices = updated)
val after = _uiState.value
println("State Equality: ${before === after}")
Which prints out:
List Equality: false
State Equality: true
I agree that the list is not equal. That was my intention. To have a distinct list just in case. What surprises me is that the value property is equal before and after the copy. Doesn't copy return a new shallow reference? Is this not how this update should occur?
So I see why I am not getting a recomposition. Compose does not see the value property change because it did not change. How do I get it to change?
You can use SnapshotMutationPolicy to control how the result of mutableStateOf report and merge changes to the state object. Take a look at my answer here:
https://stackoverflow.com/a/74301717/8389251

room LiveData how to ignore some properties change

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.

LiveData how to get the number of copies?

I have an app with multiple fragments in ViewPager.
There are RecyclerViews with data that may be repeated in other fragments.
So i decided to use one MutableLiveData for each unit (using a unique key),
without extra copy, in one repository
Data in livedata like that:
class Data {
var id = 0
var type = 0
var name = ""
var onlineStatus = OnlineStatus.GroupOrWithout
var icon: Bitmap: ?= null
}
map[data.id + data.type] = Data()
// when we need this data
fun getData(id: Int, type: Int) : MutableLiveData<Data>? {
val res = map[id + type]
if(res != null) {
return res
}
// or create new one if this fist time when we need it
....
}
But now i realized, that the traditional approach with LiveData limited to fragment's lifecycle.
has the advantage that is destroys unused data after the fragment is destroyed.
But this way looks more complicated to me, because events in another fragment
may be lost and then i will have not-synchronous data even of the same element.
So I was thinking, can I manually manipulate and destroy the MutableLiveData,
if it has no references from fragments?
Probably in the android is an analogue of shared_ptr from c++?
I really like current architecture but i don't know how to clean not used more LiveData

LiveData "pass-by-reference" initial value

I have a ViewModel class that looks like this:
class EditUserViewModel(
private val initUser: User,
) : ViewModel() {
private val _user = MutableLiveData(initUser)
val user: LiveData<User>
get() = _user
fun hasUserChanged() = initUser != _user.value
}
User can update some properties of the User data class instance through the UI.
To check if there are any changes when navigating from the fragment I use hasUserChanged method.
The problem is that is always false. I checked and it seems that the initialUser changes every time I change the _user MutableLiveData.
Why is that? Is the initial value of MutableLiveData passed by reference? I always thought that Kotlin is a "pass-by-value" type of language.
Update:
The problem seems to disappear when copying initUser before putting it inside the MutableLiveData.
private val _user = MutableLiveData(initUser.copy())
But it still doesn't make sense to me why I have to do that.
Kotlin is like java and they are pass-by-value. If you implement the equals function in User class, or make it as data class (which implements the equals function implicitly), it makes you sure that the content of the user objects is checked by != operator.
Update
If you are changing the value of LiveData directly, for example like this:
_user.value.name = "some name"
it means that you are changing the name property of the initUser, because _user.value exactly refers to the object that the initUser does. Consequently, the != operator always returns false, because we have one object with two references to it.
Now, when you are doing so:
private val _user = MutableLiveData(initUser.copy())
you are creating a deep copy of initUser (let's call it X) which is a new object in memory with the same property values of initUser.
Thus, by changing its properties like: _user.value.name = "some name", in fact, you are making this change on X, not initUser. It leads to preserving the initial values in initUser, meaning do not changing them, and solving the issue.

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.

Categories

Resources