DEV Community

Sutaek Oh
Sutaek Oh

Posted on • Originally published at Medium

ImmutableList vs. List in Jetpack Compose: Rethinking “Best Practice” After Strong Skipping Mode

Before Strong Skipping Mode became the default, it was widely considered best practice in Compose to prefer immutable collections (e.g., kotlinx ImmutableList) over unstable collections like List<T> for stability and better skipping behavior.

With Strong Skipping Mode now being the default, the situation has changed. In most real-world screens, immutable collections don't automatically outperform unstable ones - in many cases they provide little to no performance benefit, and can even become pure overhead due to conversion/allocation and equality costs.

Case-by-Case Analysis

To make this discussion concrete, I'll compare ImmutableList<T> and List<T> across three common cases.

In most Android apps, data from Retrofit, Ktor, Room, or similar libraries usually comes as List<T>, and intermediate operations like map preserve it. So using ImmutableList<T> for Compose stability typically means calling toImmutableList().

With that in mind, this post assumes:

  • List<T> is not backed by a MutableList<T> that is mutated from elsewhere.
  • ImmutableList<T> is created via toImmutableList(), not incrementally built using PersistentList<T> operations.

Also, this post only discusses Compose skippability, not the architectural benefits of immutable collections.

The list never changes (always the same instance)

If your list never changes and you keep the exact same instance, List<T> and ImmutableList<T> behave the same: an instance equality (referential equality) check (===) is enough. [1]

The list instance changes only when the content actually changes

This is the "healthy state management" case: the list changes because the data changed, and the UI should recompose.

In this scenario, using ImmutableList<T> adds extra work: you pay an O(N) conversion cost to build it (e.g., toImmutableList()), and you also pay O(N) object equality (structural equality, == or equals()) when Compose compares the new parameter to the previous one. Since you need to recompose anyway, that extra work is pure overhead - and List<T> avoids both costs.

The content doesn't change, but a new list instance is created frequently

This is the one case where ImmutableList<T> can actually help. If you keep recreating List<T> instances with the same content, Compose will treat the parameter as changed because the instance equality check fails. With ImmutableList<T>, Compose can use object equality to recognize "same content" and skip work higher up the composition tree.

The catch is the cost: equals() for lists is O(N), and if that parameter flows through multiple composable layers, you can end up paying O(N) comparisons repeatedly. If most of the UI work is already isolated in skippable leaf composables, those repeated comparisons (plus conversions) are often harder to justify than just letting List<T> flow down and relying on skipping at the leaf.

So even in this case, ImmutableList<T> isn't an automatic win.

Conclusion

If your concern is skippability, I’d recommend starting with List<T> in most cases without worrying too much. This is also true for other unstable collections like Set<T> and Map<K, V>. Optimization can wait until you actually see performance issues.

Remember: not every composable should be skippable. [2] And not every list needs to be ImmutableList<T>.

Top comments (0)