Can I use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change? - kotlin

The following content is from the article.
1: I don't understand fully if I can use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change?
2: I'm very strange why State<List<T>> and the immutable listOf() can be observed by Compose to trigger recomposition when they change but in fact List<T> and immutable listOf() are immutable, could you give me some sample codes?
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().
Image

The core concept is
Recomposition happens only when an observable state change happens.
For mutable objects, we have options to use add(), remove() and other methods and modify the object directly.
But the change would not trigger a recomposition as the change is not observable. (The object instance is NOT changed)
Even for mutable objects, we can trigger proper recomposition by assigning them to a new object instance. (The object instance is changed)
Hence using mutable objects is error-prone.
We can also, see a lint error due to this problem.
On the other hand, an immutable object like list can not be modified. They are replaced with a new object instance.
Hence they are observable and proper recomposition happens. (The object instance is changed)
Use this as an example to understand the concept.
#Composable
fun ComposeListExample() {
var mutableList: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var mutableList1: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var arrayList: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var arrayList1: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var list: MutableState<List<String>> = remember {
mutableStateOf(listOf())
}
Column(
Modifier.verticalScroll(state = rememberScrollState())
) {
// Uncomment the below 5 methods one by one to understand how they work.
// Don't uncomment multiple methods and check.
// ShowListItems("MutableList", mutableList.value)
// ShowListItems("Working MutableList", mutableList1.value)
// ShowListItems("ArrayList", arrayList.value)
// ShowListItems("Working ArrayList", arrayList1.value)
// ShowListItems("List", list.value)
Button(
onClick = {
mutableList.value.add("")
arrayList.value.add("")
val newMutableList1 = mutableListOf<String>()
mutableList1.value.forEach {
newMutableList1.add(it)
}
newMutableList1.add("")
mutableList1.value = newMutableList1
val newArrayList1 = arrayListOf<String>()
arrayList1.value.forEach {
newArrayList1.add(it)
}
newArrayList1.add("")
arrayList1.value = newArrayList1
val newList = mutableListOf<String>()
list.value.forEach {
newList.add(it)
}
newList.add("")
list.value = newList
},
) {
Text(text = "Add")
}
}
}
#Composable
private fun ShowListItems(title: String, list: List<String>) {
Text(title)
Column {
repeat(list.size) {
Text("$title Item Added")
}
}
}
P.S: Use mutableStateListOf if you have a list of items that needs to be modified as well as trigger recomposition properly.

I managed to do like this:
#Composable
fun ComposeListExample(
allObjects: List<Object>,
selectedObjects: List<Object>
) {
val selectedItems = remember {
mutableStateListOf<Object>().apply { addAll(selectedObjects) }
}
Column {
allObjects.forEach { item ->
SomeView(
title = item.title,
onSelect = {
if (selectedItems.contains(item)) {
selectedItems.remove(item)
} else {
selectedItems.add(item)
}
})
}
}
}

Related

How to Observe LiveData with custom pair class in Kotlin

I am trying to observe the LiveData for the method which returns custom pair having 2 values. I want the observable to be triggered when I change either one of the values. But it is not getting triggered. Following is the code:
CustomPair.kt
data class CustomPair<T, V>(
var first : T,
var second : V
)
Observable:
falconViewModel.getPlanet1Name().observe(this) {
planet1.text = it.first
planet1.isEnabled = it.second
}
Getter and setter methods in ViewModel falconViewModel
private val planet1EnabledAndText = MutableLiveData<CustomPair<String, Boolean>>()
fun getPlanet1Name() : LiveData<CustomPair<String, Boolean>> {
return planet1EnabledAndText
}
fun setPlanet1Name(planetName : String, visibility : Boolean) {
planet1EnabledAndText.value?.run {
first = planetName
second = visibility
}
}
Can't we observe the value in such case? Please help what is wrong here.
It started working when I tried to set a new value of CustomPair instead of modifying the existing values in the object.
Replaced
planet1EnabledAndText.value = CustomPair(planetName, visibility)
with
planet1EnabledAndText.value?.run {
first = planetName
second = visibility
}
and it worked.

Unable to trigger re-composition after re-assigning mutableStateList inside a coroutine scope

I'm still a bit of a beginner with Jetpack compose and understanding how re-composition works.
So I have a piece of code calls below inside a ViewModel.
SnapshotStateList
var mutableStateTodoList = mutableStateListOf<TodoModel>()
private set
during construction of the view model, I execute a room database call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList = listTypeTodo.toMutableStateList()
}
}
}
then I have an action from a the ui that triggers adding of a new Todo to the list and expecting a re-composition from the ui that shows a card composable
fun onFabClick() {
todoList.add(TodoModel())
}
I can't figure out why it doesn't trigger re-composition.
However if I modify the init code block below, and invoke the onFabClick() action, it triggers re-composition
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList.addAll(listTypeTodo)
}
}
}
or this, taking out the re-assigning of the mutableStateList outside of the coroutine scope also works (triggers re-composition).
init {
// just trying to test a re-assigning of the mutableStateList property
mutableStateTodoList = emptyList<TodoModel>().toMutableStateList()
}
Not quite sure where the problem if it is within the context of coroutine or SnapshotStateList itself.
Everything is also working as expected when the code was implemented this way below, using standard list inside a wrapper and performing a copy (creating new reference) and re-assigning the list inside the wrapper.
var todoStateWrapper by mutableStateOf<TodoStateWrapper>(TodoStateWrapper)
private set
Same init{...} call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
todoStateWrapper = todoStateWrapper.copy (
todoList = listTypeTodo
)
}
}
}
To summarize, inside a coroutine scope, why this works
// mutableStateList
todoList.addAll(it)
while this one does not?
// mutableStateList
todoList = it.toMutableStateList()
also why does ordinary list inside a wrapper and doing copy() works?
The mutable state in Compose can only keep track of updates to the containing value. Here is simplified code on how MutableState could be implemented:
class MutableState<T>(initial: T) {
private var _value: T = initial
private var listeners = MutableList<Listener>
var value: T
get() = _value
set(value) {
if (value != _value) {
_value = value
listeners.forEach {
it.triggerRecomposition()
}
}
}
fun addListener(listener: Listener) {
listeners.add(listener)
}
}
When the state is used by some view, this view subscribes to updates of this particular state.
So, if you declare the property as follows:
var state = MutableState(1)
and try to update it with state = 2.toMutableState() (this is analogous to your mutableStateTodoList = listTypeTodo.toMutableStateList()), triggerRecomposition cannot be called because you create a new object which resets all the listeners. Instead, to trigger recomposition you should update it with state.value = 2.
With mutableStateList, the analog of updating value is any method of MutableList interface that updates containing list, including addAll.
Inside init it works because no view is subscribed to this state so far, and that's the only place where methods such as toMutableStateList should be used.
It is important to always define mutable states as immutable property with val in order to prevent such mistakes. To make it mutable only from view model, you can define it like this, and make updates on _mutableStateTodoList:
private val _mutableStateTodoList = mutableStateListOf<TodoModel>()
val mutableStateTodoList: List<TodoModel> = _mutableStateTodoList
The only exception when you can use var is using mutableStateOf with delegation - this is where you can use it with private set because in that case the delegation does the work for you by not modifying the container, but only it's value property. Such method cannot be applied to mutableStateListOf, because there's no single value field that's responsible for the data in case of list.
var someValue by mutableStateOf(1)
private set

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.

Kotlin / value passing to List<>()

I have a question in List<Contact>() I'm asked to pass init and size. I'm not sure if it's obligated to pass it as in my following tutorial ArrayList<String>() was empty, maybe it's because I was using List<>? Also, it doesn't recognize lowercase() and add() is it also related to List<>?
Code Snippet
val contacts = remember { DataProvider.contactList }
var filteredContacts: List<Contact>
val textState = remember { mutableStateOf(TextFieldValue("")) }
LazyColumn(
...
) {
val searchText = textVal.value.text
filteredContacts = if (searchText.isEmpty()){
contacts
}
else{
val resultList = List<Contact>()
for (contact in contacts) {
if (contact.lowercase(Locale.getDefault()).contains(searchText.lowercase(Locale.getDefault()))) {
resultList.add(contact)
}
}
resultList
}
In kotlin, List has no add method. For that you would need to have a MutableList.
Regarding lowercase method, this is available for Strings. You are trying to apply that to a Contact object, which I guess has no lowercase method.

questions about DI, ViewModel etc

I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.