DEV Community

Zach Klippenstein
Zach Klippenstein

Posted on • Updated on

Two mutables don’t make a right

We’ve all done it: put some data in a list, changed the data around a little, and rendered the list in some UI. Unfortunately, sometimes the UI doesn’t update when you change the data. This post isn’t going to try to cover all the reasons why that might be, but just the most common one, which I’ve seen come up at least a couple times a week.

This post is largely applicable to any reactive programming library, although it does focus a little extra on Compose in spots. It also focuses on collections, but applies to any data structures you're using to hold state.

tl;dr

If someone sent you a link to this post as a reply to a question on Slack or somewhere, and you just want a quick answer, here you go:


Don't put mutable collections inside mutable state holders.


State holders like MutableStateFlow, and MutableState only send change notifications when the old and new state values are not equal.

  • They will not send change notifications if you simply change some properties of the object they're holding.
  • They will not send change notifications if you mutate the object they're holding, then set that same object as the new state. Because the old and new state objects are actually the same object, they will always be considered equal (this is part of the contract of the equals method in Kotlin and Java).

The correct thing to do is to either:

  1. Store an immutable collection in the state holder, and update the collection by creating a new copy. The easiest way to do this is to use the standard Kotlin read-only collections, like listOf(), and the standard operators on those collections, like plus.
  2. Use a special collection type that can send its own change notifications when you mutate it. This only works if you're using Compose's state system, because RxJava, LiveData, Flow, etc. can't provide such collection types. Compose provides two collection types for this: mutableStateListOf<T>(): SnapshotStateList<T> and mutableStateMapOf<K, V>(): SnapshotStateMap<K, V>. If you really want to use one of these but need a Set, you can just use a SnapshotStateMap with a value type of Unit.

DON'T DO

var list by mutableStateOf(mutableListOf("a"))
list.add("b")
Enter fullscreen mode Exit fullscreen mode

This is just as useless:

var list by mutableStateOf(mutableListOf("a"))
val tempList = list
tempList.add("b")
list = tempList
Enter fullscreen mode Exit fullscreen mode

because list and tempList refer to the same object, so list = tempList is a no-op. That's the same as doing this:

var list by mutableStateOf(mutableListOf("a"))
list.add("b")
list = list
Enter fullscreen mode Exit fullscreen mode

DO

var list by mutableStateOf(listOf("a"))
list = list + "b"
Enter fullscreen mode Exit fullscreen mode

Or

val list = mutableStateListOf("a")
list.add("b")
Enter fullscreen mode Exit fullscreen mode

Hopefully that helps, but if you'd like to know why this is the case, please read on.

Degrees of mutability

When writing stateful code, I find it helpful to think about the number of ways a single value can be changed. I'm not sure if there's a standard term for this concept, so I'll just call it the "degree of mutability". That number should never be more than one. Consider this code:

var data = mutableStateOf(mutableListOf("a"))
Enter fullscreen mode Exit fullscreen mode

If you found this in a codebase, and you had to update this value, how would you do it? You've got three options:

  1. By re-assigning data to point to a different MutableState: data = otherMutableState
  2. By changing the value of the MutableState: data.value = otherList
  3. By changing the list itself: data.value.add("b")

This is bad for multiple reasons: it's not clear to readers of the code how they should change the value, and it means it's not safe to pass any of the nested values around because some other code might decide to just re-assign data. In this example, the only "correct" place to mutate is the value of the MutableState, since that's the only one that will actually cause change notifications to be sent.

Let's dig into the two main concepts at play here: mutable data structures and observing state changes.

Modifying collections

The Kotlin standard library has two list types: List<T> and MutableList<T>.

For brevity I’ll limit the discussion in this section to lists, although the same principles apply to other data structures like maps and sets.

MutableList, as the name implies, is a list that has operations to mutate, or change, its contents: add, remove, and replace items. It’s easy to come to the conclusion that the List type must therefore be immutable. That’s not the case. Lists are ‘read-only’, but they may or may not be mutable. Given a reference to a List, you can’t change its contents yourself, but some other code might be able to. The MutableList interface extends the List interface, so it’s very easy to create a list that you can change, but pass it around to other code so that code can only read it, even as you’re still making changes. For example:

class Model {
  private val mutableData: MutableList<String> = mutableListOf("a")
  val readOnlyData: List<String> = mutableData
}
Enter fullscreen mode Exit fullscreen mode

That said, Kotlin still provides operators like plus and minus for Lists – even though they’re read-only! It can do this because these operators actually copy the list and return a new instance of the list, instead of mutating the list object in-place. This code demonstrates:

/// Read-only Lists

val list = listOf("a")
// Must be a var since we'll assign a new instance.
var longerList = list
longerList = longerList + "b"

// The !== operator compares references.
assert(list !== longerList) 
assert(list != longerList)
assert(longerList == listOf("a", "b"))

/// Mutable Lists

val mutableList = mutableListOf("a")
// Can be a val since we'll modify in-place.
val longerMutableList = mutableList
longerMutableList.add("b")

assert(mutableList === longerMutableList)
// Both lists point to the same object, so they're equal.
assert(mutableList == longerMutableList)
assert(mutableList == mutableListOf("a", "b"))
Enter fullscreen mode Exit fullscreen mode

Now that we've reviewed how collection mutation and comparison interact, let's review how common libraries manage state changes.

Observing state changes

In order for a UI to update when data changes, it needs to be notified when changes happen. Some common tools for doing this on Android are libraries like RxJava and LiveData, as well as Kotlin's Flows and, more recently, Compose’s snapshot state system. All these options actually have very similar APIs for this particular task and they all look something like this:

val someValue: StateHolder<ValueType> = StateHolder(initialValue)
Enter fullscreen mode Exit fullscreen mode

The library provides some type that holds a reference to some value that represents a piece of “state” in your program. That holder type lets you do two things: (1) change the value, and (2) subscribe to be notified when the value changes. Two key concepts here are “state” and “change”. If you look at a state value at two different times, and it’s the same value, then from your perspective the state hasn’t changed. Even if, in the meantime, a whole bunch of work has happened behind the scenes to recalculate what the state value is and determined that it should be… the same value. This concept is often referred to in a number of ways, with terms like "idempotency", "conflation", or "de-duping". In all of the aforementioned libraries, setting the state to the current value won't actually send any change notifications.

In practice, this is actually a performance optimization because when the UI is derived from some state, then given two equivalent state values, the same UI should be derived. If the state value hasn't changed, there's no need to update anything in the UI, because nothing would actually change.

Given the pseudo-types from the above code, and the behavior of the libraries mentioned above, this code would send a change notification:

val name = StateHolder("a")
// Change state to a new value
name.value = "b"
Enter fullscreen mode Exit fullscreen mode

but this code would not:

val name = StateHolder("a")

// "Change" the state to the same value…
// that is, don't change it at all.
name.value = "a"
Enter fullscreen mode Exit fullscreen mode

Which implies that this code would also not send change notifications:

class Model(var name: String)

val model = StateHolder(Model("a"))
val tempModel = model.value

// The name property is a var, so we can just change it.
// However, StateHolder has no way of knowing when
// properties of objects it's holding change.
tempModel.name = "b"

// model.value === tempModel, which implies that
// model.value == tempModel, so this is also a no-op.
model.value = tempModel
Enter fullscreen mode Exit fullscreen mode

That code is effectively the same as this, which makes the issue more obvious:

val model = StateHolder(Model("a"))
model.value.name = "b"
model.value = model.value
Enter fullscreen mode Exit fullscreen mode

When you change the properties inside the Model, you're mutating the same object that the holder already knows about. So when it does its internal comparison of the current value to the new one, it's comparing the same object to itself, and the contract of Kotlin's equals method requires that an object is always "equal" to itself.

Collections as state

The above Model type was a thinly-veiled placeholder for MutableList, MutableMap, or any other standard mutable collection type. Substitute MutableList for Model and you'll get the bad example code at the top of this post. Storing a mutable object inside a state holder, then mutating the object, will not send change notifications unless the mutable object sends its own change notifications.

There are two alternatives, as described near the top of this post, and they each have their own benefits and trade-offs.

Immutable collections

Immutable collections are, generally, easier to reason about in concurrent code because given an instance of a collection, you can assume that collection will never change until the heat-death of the universe. When using reactive libraries like RxJava or LiveData, using immutable collections is your only real option. And while it might sound expensive, in many cases copying small collections is actually very fast and not worth worrying about.

Snapshot-aware collections

Compose provides an alternative to immutable collections: collections which actually send their own change notifications. These collections implement the standard Kotlin Mutable* interfaces, and can be changed in-place. When modified, they tell the snapshot state system that anything that read the collections should be invalidated and recalculated. This is possible because Compose's snapshot state system doesn't require state holders to have any explicit API for observing changes. For example, this is perfectly acceptable code:

val list: MutableList<String> = mutableStateListOf("a")
list.add("b)
Enter fullscreen mode Exit fullscreen mode

Note that list is a val, not a var, because we only want to mutate the list itself, and not change the instance of the list to which the name list refers. Any code reading list in a Composable (or any other snapshot-aware context) will automatically be notified when the list is changed. Also note that list has type MutableList – aside from how the list is created, the mutableStateListOf() function, it's just a standard Kotlin list. For more information about how Compose's snapshot state system works, see this blog post, as well as the other posts in this series.

Under the hood, the mechanism by which these "snapshot state collections" work is actually simpler than you might suspect. They use immutable collections! mutableStateListOf returns a MutableList that effectively stores a private reference to a special immutable list inside a MutableState. When you modify the list, it creates a copy of the private immutable list with the change and stores it in a MutableState. The trick here is that the immutable list isn't just a regular ArrayList, but an special implementation that is "persistent" – that's a fancy term from the world of functional programming that means that when the list is copied to be modified, only a small part of the list is actually copied, and most of the list ends up just being a reference to the old list. This means that even large collections can be copied and mutated without wasting space or time. Persistent data structures are a fun topic but too big to get into more here, so check out the Wikipedia page for more information. Kotlin doesn't include any persistent lists in its standard library, but does provide a separate kotlinx library, kotlinx.collections.immutable, which is actually what Compose uses.

Conclusion

One last thing: This post isn't only relevant for collections or other "container" data structures, it applies to any classes you're using to hold state. Any class can have mutable properties (vars), and putting an instance of such a class into a mutable state holder has all the same issues as storing a mutable collection. If you're using Compose, you can use MutableStates to back those vars and still get change notifications, but, once again, any time you've got multiple ways to set the same value ("degree of mutability" higher than 1), it's confusing for readers of your code, users of your APIs, and asking for bugs.

If you came here wondering why your UI wasn't updating when you changed a list, I hope this post has not only answered your question, but also explained how to think about stateful data flow. Always think carefully about which parts of your data are mutable and immutable.

Please let me know if anything was unclear or if you have any further questions in the comments!

Update: BehaviorRelay (and BehaviorSubject) aren't actually idempotent. Thanks for reminding me @mzmzgreen!

Discussion (2)

Collapse
andreandyp profile image
André Michel Andy

I have found this article when looking for some info about mutableStateListOf because a problem I had with Compose. Instead of just looking for the answer, I started to read all the series. And I have to say that all articles are great! Now I learned really useful concepts about how Compose works (in terms of state).

Also, about this article specifically, it is really useful! I think most of people will encounter this problem when displaying a mutable list, and this explanation is excellent. 😁

Collapse
mochadwi profile image
Mochamad Iqbal Dwi Cahyo

Thanks for follow up the discussion about mutable collection used inside mutableStateOf. Your compose article always giving us the proper guidance to use Compose!