Android LiveData/StateFlow List Item Property Update Issue - android

So I'm updating my RecylerView with StateFlow<List> like following:
My data class:
data class Student(val name: String, var isSelected: Boolean)
My ViewModel logic:
fun updateStudentsOnSelectionChanged(targetStudent: Student) {
val targetIndex = _students.value.indexOf(targetStudent)
val isSelected = !targetStudent.isSelected
_students.value[targetIndex].isSelected = isSelected //<- doesn't work
}
Problem: The UI is not changed, but the isSelected inside _student is changed, what's going on? (same to LiveData)

I assume _students is a StateFlow<List>. Changing the isSelected property of the Student model doesn't trigger the StateFlow. The workaround would be to make the isSelected property of the Student data class immutable to get it compared when new state is set, create a MutableList out of the current list and copy the existing Student object with the new value for isSelected property:
data class Student(val name: String, val isSelected: Boolean)
val students = _students.value.toMutableList()
students[targetIndex] = students[targetIndex].copy(isSelected = isSelected)
_students.value = students

Ok, thanks to #Tenfour04 and #Sergey, I finally found out that StateFlow/LiveData cannot detect the internal changes, that's because they are both actually comparing the Reference of the .value.
That means, If I want to force the StateFow<List> to update, the only way is to assign a new List to it, therefore I created the following helper extension function:
fun <T> List<T>.mapButReplace(targetItem: T, newItem: T) = map {
if (it == targetItem) {
newItem
} else {
it
}
}
//this function returns a new List with the content I wanted
In Fragment:
val itemCheckAction: (Student) -> Unit = { student ->
val newStudent = student.copy(isSelected = !student.isSelected) //prepare a new Student
viewModel.updateStudentsOnSelectionChanged(student, newStudent) //update the StateFlow
}
In ViewModel:
fun updateStudentsOnSelectionChanged(currentStudent: Student, newStudent: Student) {
val newList = _students.value.mapButReplace(currentStudent, newStudent)
_students.value = newList //assign a new List with different reference
}

Related

How to update mutable state flow with complex list of objects?

Assume we have one data class called ProductCategory which contains the product category's name and a list of products for this category.
Product Category class:
data class ProductCategory(
val id: String,
val name: String,
var products: List<Product>
)
Product class:
data class Product(
val id: String,
val name: String,
val productId: String,
var isFavorite: Boolean = false,
)
After calling an API to load the data and keep it on a Mutable State Flow object,
ViewModel class:
private var _productCategories = MutableStateFlow<List<ProductCategory>>(listOf())
val productCategories : StateFlow<List<ProductCategory>> = _productCategories.asStateFlow()
init {
viewModelScope.launch {
repository.loadProducts()
.flowOn(Dispatchers.IO)
.collect { products ->
_productCategories.update { products }
}
}
}
When user clicks the favorite button I need to update the field isFavorite. The problem is that if I try to update mutable state flow like this UI won't be notified for any change because the object that is being updated is the same with the current (we apply the change here on the same object)
_productCategories.update {
_productCategories.value.toMutableList().apply {
get(productCategoryIndex).products.apply {
get(productIndex).isFavorite = true
}
}
}
I tried to apply changes using a deep copied object and then update mutable state flow and it worked but it seems to be overkill... what can I do?

How to remove a object from a data class in kotlin

I want to remove a object from the list and so that i can add just required string and pass it . i have a model class like this
data class TagItem(
val tagTittle: String,
val isSelected: Boolean
)
this data class is mapped for the Lazy Column for making the list select and deSelect the items
var tagsItems by remember {
mutableStateOf(
(tagsList).map {
TagItem(
tagTittle = it,
isSelected = false
)
}
)
}
val productEveryTags = tagsItems.filter {
it.isSelected
}
Log.i(TAG,"Only this $productEveryTags ")
viewModel.onEvent(ProductUploadEvent.EnteredProductTags(productEveryTags))
i am filtering the selected items alone but in my log statement
Only this [TagItem(tagTittle=Tagged , isSelected=true), TagItem(tagTittle=Ducati , isSelected=true)]
How can i remove the "isSelected" object and just get the "tagTittle" alone into a single List
You can simply map your instances for the output:
Log.i(TAG,"Only this ${productEveryTags.map { it.tagTittle }}")
Or combine it with the existing filter. Depending on whether you are interested in duplicates, you can also directly map to a set:
val productEveryTags = tagsItems.filter {
it.isSelected
}.mapTo(LinkedHashSet()) {
it.tagTittle
}
Log.i(TAG,"Only this $productEveryTags")

Jetpack Compose recompostion of property change in list of objects

I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.

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)

How to programically trigger notify on MutableLiveData change

I have a LiveData property for login form state like this
private val _authFormState = MutableLiveData<AuthFormState>(AuthFormState())
val authFormState: LiveData<AuthFormState>
get() =_authFormState
The AuthFormState data class has child data objects for each field
data class AuthFormState (
var email: FieldState = FieldState(),
var password: FieldState = FieldState()
)
and the FieldState class looks like so
data class FieldState(
var error: Int? = null,
var isValid: Boolean = false
)
When user types in some value into a field the respective FieldState object gets updated and assigned to the parent AuthFormState object
fun validateEmail(text: String) {
_authFormState.value!!.email = //validation result
}
The problem is that the authFormState observer is not notified in this case.
Is it possible to trigger the notification programically?
Maybe you can do:
fun validateEmail(text: String) {
val newO = _authFormState.value!!
newO.email = //validation result
_authFormState.setValue(newO)
}
You have to set the value to itself, like this: _authFormState.value = _authFormState.value to trigger the refresh. You could write an extension method to make this cleaner:
fun <T> MutableLiveData<T>.notifyValueModified() {
value = value
}
For such a simple data class, I would recommend immutability to avoid issues like this altogether (replaces all those vars with vals). Replace validateEmail() with something like this:
fun validateEmail(email: String) = //some modified version of email
When validating fields, you can construct a new data object and set it to the live data.
fun validateFields() = _authFormState.value?.let {
_authFormState.value = AuthFormState(
validateEmail(it.email),
validatePassword(it.password)
)
}

Categories

Resources