MutableStateFlow not firing when add item with ViewModel - kotlin

I need to observe/collect data from Stateflow inside ViewModel when the add method is called. But when I add an item, the state doesn't do recomposition.
This is my ViewModel code:
private var _selectedData = MutableStateFlow<MutableList<String>>(mutableListOf())
val data = listOf("Hello", "my", "name", "is", "Claire", "and", "I", "love", "programming")
val selectedData = _selectedData.asStateFlow()
fun addSelectedData(index: Int){
_selectedData.value.add(data[index])
Log.d("TAG", _selectedData.value.toString())
}
And this is my composable code:
val mainViewModel: MainViewModel = viewModel()
val selectedData by mainViewModel.selectedData.collectAsState()
LaunchedEffect(key1 = selectedData){
Log.d("TAG", "SELECTED $selectedData")
}
Thanks in advance.

MutableStateFlow is not able to notice that you modified the object that it holds. You have to set a new object to MutableStateFlow, so a new list.
Change the type of _selectedData to MutableStateFlow<List<String>> and then do:
_selectedData.value = _selectedData.value + data[index]
You can make it shorter with:
_selectedData.value += data[index]

Related

Jetpack compose collectAsState() is not collecting a hot flow when the list is modified

When I use collectAsState(), the collect {} is triggered only when a new list is passed, not when it is modified and emitted.
View Model
#HiltViewModel
class MyViewModel #Inject constructor() : ViewModel() {
val items = MutableSharedFlow<List<DataItem>>()
private val _items = mutableListOf<DataItem>()
suspend fun getItems() {
_items.clear()
viewModelScope.launch {
repeat(5) {
_items.add(DataItem(it.toString(), "Title $it"))
items.emit(_items)
}
}
viewModelScope.launch {
delay(3000)
val newItem = DataItem("999", "For testing!!!!")
_items[2] = newItem
items.emit(_items)
Log.e("ViewModel", "Updated list")
}
}
}
data class DataItem(val id: String, val title: String)
Composable
#Composable
fun TestScreen(myViewModel: MyViewModel) {
val myItems by myViewModel.items.collectAsState(listOf())
LaunchedEffect(key1 = true) {
myViewModel.getItems()
}
LazyColumn(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 10.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(myItems) { myItem ->
Log.e("TestScreen", "Got $myItem") // <-- won't show updated list with "999"
}
}
}
I want the collect {} to receive the updated list but it is not. SharedFlow or StateFlow does not matter, both behave the same. The only way I can make it work is by creating a new list and emit that. When I use SharedFlow it should not matter whether equals() returns true or false.
viewModelScope.launch {
delay(3000)
val newList = _items.toMutableList()
newList[2] = DataItem("999", "For testing!!!!")
items.emit(newList)
Log.e("ViewModel", "Updated list")
}
I should not have to create a new list. Any idea what I am doing wrong?
You emit the same object every time. Flow doesn't care about equality and emits it - you can try to collect it manually to check it, but Compose tries to reduce the number of recompositions as much as possible, so it checks to see if the state value has actually been changed.
And since you're emitting a mutable list, the same object is stored in the mutable state value. It can't keep track of changes to that object, and when you emit it again, it compares and sees that the array object is the same, so no recomposition is needed. You can add a breakpoint at this line to see what's going on.
The solution is to convert your mutable list to an immutable one: it's gonna be a new object each on each emit.
items.emit(_items.toImmutableList())
An other option to consider is using mutableStateListOf:
private val _items = mutableStateListOf<DataItem>()
val items: List<DataItem> = _items
suspend fun getItems() {
_items.clear()
viewModelScope.launch {
repeat(5) {
_items.add(DataItem(it.toString(), "Title $it"))
}
}
viewModelScope.launch {
delay(3000)
val newItem = DataItem("999", "For testing!!!!")
_items[2] = newItem
Log.e("ViewModel", "Updated list")
}
}
This is the expected behavior of state and jetpack compose. Jetpack compose only recomposes if the value of the state changes. Since a list operation changes only the contents of the object, but not the object reference itself, the composition will not be recomposed.

Why can the author reassign a new object to a val via update?

The Code A is from offical sample code here.
The private val _uiState is val, in my mind, a val can be only assigned a object for one time.
It seems that _uiState.update { it.copy(loading = true) } shows _uiState is assigned to a new object again by update.
I don't understand why the author can reassign a new object to a val via update, could you tell me?
Code A
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
val loading: Boolean = false,
)
class InterestsViewModel(
private val interestsRepository: InterestsRepository
) : ViewModel() {
// UI state exposed to the UI
private val _uiState = MutableStateFlow(InterestsUiState(loading = true))
val uiState: StateFlow<InterestsUiState> = _uiState.asStateFlow()
private fun refreshAll() {
_uiState.update { it.copy(loading = true) }
...
}
...
}
data class InterestsUiState(
val topics: List<InterestSection> = emptyList(),
val people: List<String> = emptyList(),
val publications: List<String> = emptyList(),
val loading: Boolean = false,
)
/**
* Updates the [MutableStateFlow.value] atomically using the specified [function] of its value.
*
* [function] may be evaluated multiple times, if [value] is being concurrently updated.
*/
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
Added Content
To Chaoz: Thanks!
But all members in data class InterestsUiState(...val loading: Boolean = falseļ¼‰is val type, and you can't change any member vaule when you have created the object of InterestsUiState.
So I can't still understand why the member value of _uiState can be changed when the author launch _uiState.update { it.copy(loading = true) }.
And more
_uiState.update { it.copy(loading = true) } is equal to
_uiState.value = _uiState.value.copy(loading = true), right?
The val keyword only refers to which object the variable holds, not the data inside said object. For example:
class MyClass(var value: Int)
The following code is not allowed:
val obj = MyClass(5)
obj = MyClass(7) // compile error
because the val keyword refers to the variable itself being reassigned to a different object. This code, however, is allowed:
val obj = MyClass(5)
obj.value = 7
Here, obj is still the same object, only a property of said object changed value. In your provided code, the update function modifies data stored inside the _uiState object, however it does not swap it for a new object. This is important because:
var obj = MyClass(5)
val copy = obj
obj = MyClass(7)
println(copy.value) // prints 5
println(obj.value) // prints 7
When reassigning a variable, the old object remains, and any other variables referencing that object are not updated. In your case, _uiState.value is modified, not the variable itself. Hope this clears things up!
Edit:
Yes, it.copy() is an expression which creates a new object. However, this code is executed in the line _uiState.update { it.copy(loading = true) }, in the refreshAll() function. As it is the last statement in a lambda expression (also the only one, but doesn't matter), it is the return value of said lambda. Here we have to look at the declaration of the update function.
The lambda is stored in the function variable (of type (T)->T). This means, whenever function() is called, the code inside the lambda is executed, and its result (a new object) is then returned by the function() call. This value is assigned to the val nextValue variable and not to _uiState itself. The compareAndSet function modifies _uiState.value and does not change the object the _uiState variable references.
And by the way, the object returned by it.copy() is of type T, and not of type MutableStateFlow<T>, which your _uiState variable holds. T is the type of _uiState.value.

Two Spinners Populated By Same ArrayList

So I am trying to populate two Spinners in the same Fragment, both using the same list, but to display different items.
I have the following data class:
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
}
The Spinner is populated in the Fragment when the list is observed from the ViewModel as below:
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, productTypeArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
This shows a list of product types as I have specified this in the toString()of the object, but is there a way to direct a second Spinner to show a list ofproduct group?
If you don't need to retrieve the values from the spinners, it's easiest to map the values to a new list:
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
//...
val adapter2 = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item,
productTypeArrayList.map(ProductTypeObject::productGroup)
//...
}
})
If you need both Spinners to be able to retrieve the original item type, then you can't use the ArrayAdapter class as is, since it relies purely on the toString() of your class. You can subclass it like this for a more flexible version that lets you pass property or lambda that is used instead of toString(). I didn't test it, but I think it will do what you want. If you use this class, you don't need to override toString() in your original data class.
class CustomArrayAdapter<T : Any>(
context: Context,
items: List<T>,
val itemToCharSequence: T.() -> CharSequence = Any::toString
) : ArrayAdapter<T>(context, 0, items) {
private val inflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (convertView ?: inflater.inflate(android.R.layout.simple_spinner_item, parent, false))
.apply {
val item = getItem(position)!! // will never be null inside getView()
(this as TextView).text = itemToCharSequence(item)
}
}
}
Usage:
val typeAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productType)
val groupAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productGroup)

Filter Observed ViewModel data for Spinner based on another Spinners selection (LiveData)

I have two Spinners, each populated by ArrayList, which is observed in the ViewModel from the Fragment as below:
InventoryAddEdit Fragment
// Observe ProductGroups and populate Spinner
businessViewModel.allAppDataProductGroups.observe(viewLifecycleOwner, { productGroupArrayList -> // ArrayList<ProductGroupObject>
if (!productGroupArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productGroupArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, { productTypeArrayList -> // ArrayList<ProductTypeObject>
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, productTypeArrayList)
binding.inventoryAddEditProductTypeSpinner.adapter = adapter
binding.inventoryAddEditProductTypeSpinner.setSelection(17) // Sets default value
}
})
I am trying to avoid keeping data in the Fragment (MVVM), so I am wondering how best to filter the second ArrayList based on the selection of the first.
I thought I could use the onItemSelected method on the first spinner to cancel the observer and reattach, but then filter the newly observed ArrayList by a selection of the first spinner. However, this seems a bit clunky. Another idea was to create another filtered list in the ViewModel, but that will mean more data in the ViewModel.
Is there another option I am missing, please?
For info, the ProductGroupObject and ProductTypeObject look like this:
ProductGroupObject
#IgnoreExtraProperties
data class ProductGroupObject (
//ProductGroup fields (1 fields)
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productGroupID: String = ""
) : Serializable {
override fun toString(): String {
return productGroup
}
}
ProductTypeObject
#IgnoreExtraProperties
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
fun detailsText(): String {
val detailsString = StringBuilder()
if(productTypeID.isNotEmpty()) detailsString.append("$productTypeID\n")
if(productType.isNotEmpty()) detailsString.append("$productType\n")
if(productGroup.isNotEmpty()) detailsString.append("$productGroup\n")
return detailsString.toString()
}
}
So the best solution I came up with was to create a 'full list' and 'filtered list' for each Spinner data set in the ViewModel + 'current selection' object for each Spinner (also kept in the ViewModel).
The 'full list' is populated by the Cloud Database on startup, the 'filtered lists' are filtered depending on the Spinner selection by way of the following code:
binding.inventoryAddEditProductGroupSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent:AdapterView<*>?, view: View?, position: Int, id: Long){
val productGroupObject = parent?.selectedItem as ProductGroupObject
if (productGroupObject.productGroupID.isNotEmpty()){
businessViewModel.updateCurrentProductGroupVMLiveData(productGroupObject.productGroupID)
}
}
This updates the 'filtered lists' (filtering the 'full list') which hold data to any of the linked Spinners as below:
fun updateCurrentProductGroupVMLiveData (currentProductGroupId: String) {
val newProductGroup = allAppDataProductGroups.value?.find { productGroup -> productGroup.productGroupID == currentProductGroupId }
_currentProductGroup.value = newProductGroup
if(newProductGroup?.productGroup != null) {
val filteredProductsList = allAppDataProductTypes.value?.filter { productTypeObject -> productTypeObject.productGroup == newProductGroup.productGroup} as ArrayList<ProductTypeObject>
_filteredAppDataProductTypes.value = filteredProductsList
}
// UPDATE OTHER SPINNERS HERE
Log.d(TAG, "updateCurrentProductGroupVMLiveData(): '_currentProductGroupId.value' updated ($currentProductGroupId)")
}
One issue I faced was that the View was nullable because of how LiveData works, which was solved in the following post: Spinner Listener LiveData Issue

Can I monitor a variable in Kotlin?

I hope to monitor a variable, I will do sometings when the variable changed, maybe just like Code A.
How can I write these code in Kotlin? Thanks!
Code A
var myList: List<Int>
registerMonitorVar(myList)
fun onVariableChange(){
if (myList.size>=1){
btnDelete.enabled=true
}
}
To ice1000
Thanks! but the following code doesn't work! I don't know how to init allList when I need set property.
private lateinit var allList: MutableList<MSetting> set(value) {
field = value
onVariableChange()
}
private var allList=mutableListOf<MSetting>() set(value) {
field = value
onVariableChange()
}
fun onVariableChange(){
if (allList.size>=1){
}
}
To humazed:
Thanks! why isn't the following code correct?
private var allList: MutableList<MSetting> by Delegates.vetoable(mutableListOf<MSetting>())
{ property, oldValue, newValue ->
{
btnRestore.isEnabled=(newValue.size >= 1)
btnBackup.isEnabled=(newValue.size >= 1)
}
}
To humazed and ice1000
Thanks! The system can't monitor the change of the var allList when I use Code 2
private var allList: MutableList<MSetting> by Delegates.observable(mutableListOf<MSetting>())
{ property, oldValue, newValue ->
btnRestore.isEnabled = newValue.size >= 1
}
Code 1
allList=SettingHandler().getListAllSetting().toMutableList() // Observable Work
Code 2
allList.clear()
allList.addAll(SettingHandler().getListAllSetting().toMutableList()) //Observable Doesn't Work
Kotlin observable and vetoable is perfect for this use case.
vetoable is doing just what you want. from the doc:
vetoable:
Returns a property delegate for a read/write property that
calls a specified callback function when changed, allowing the
callback to veto the modification.
for your example, you can use:
var myList: List<Int> by Delegates.vetoable(listOf()) { property, oldValue, newValue ->
if (newValue.size >= 1)
true // apply the change to myList
else
false // ignore the change. ie. myList stay the same.
}
or simply:
var myList: List<Int> by Delegates.vetoable(listOf()) { property, oldValue, newValue ->
newValue.isNotEmpty()
}
After your edit. I see in your next example observable is more suitable as you seem to want the list to be changed regardless of any condition.
var allList: MutableList<String> by Delegates.observable(mutableListOf<String>()) { property, oldValue, newValue ->
btnRestore.isEnabled = newValue.size >= 1
btnBackup.isEnabled = newValue.size >= 1
}
your code didn't work because you added unnecessary {} and used vetoable without returning neither true nor false.
For the edited answer. it deserves its own question but I'm going to answer it here anyway.
you could use list and when you want to change the list replace it with the new list. this has performance implications since you creating a new list every time you need to add or remove an item.
or you could extend the list class or use extension functions to react to add and delete operations. ex:-
fun main(args: Array<String>) {
val myList = mutableListOf<Int>()
myList.addAllAndNotify(listOf(1, 2, 3, 4))
myList.addAllAndNotify(listOf(1, 2, 88, 9))
}
fun <E> MutableList<E>.addAllAndNotify(elements: Collection<E>) {
addAll(elements)
doStuff(this)
}
fun <E> doStuff(list: List<E>) {
println("list = ${list}")
}
the output:
list = [1, 2, 3, 4]
list = [1, 2, 3, 4, 1, 2, 88, 9]
finally, you could take a look at this nice lib which has ObservableList if this what you need you better of using it instead of writing it yourself.
You can override the and setter.
var myList: List<Int> // maybe here's a missing initialization
set(value) {
field = value
onVariableChange()
}
fun onVariableChange() {
if (myList.size >= 1) {
btnDelete.enabled = true
}
}
By this, if you do myList = blabla, onVariableChange will be called.
To the edit, why doesn't
private var allList = mutableListOf<String>()
set(value) {
field = value
onVariableChange()
}
fun onVariableChange() {
if (allList.size >= 1) {
}
}
This code work?
To the comment, you may use this:
private var allList = listOf<String>()
set(value) {
field = value
onVariableChange()
}