Jetpack Compose @Stable List<T> parameter recomposition
Asked Answered
C

5

14

@Composable functions are recomposed

  • if one the parameters is changed or
  • if one of the parameters is not @Stable/@Immutable

When passing items: List<Int> as parameter, compose always recomposes, regardless of List is immutable and cannot be changed. (List is interface without @Stable annotation). So any Composable function which accepts List<T> as parameter always gets recomposed, no intelligent recomposition.

How to mark List<T> as stable, so compiler knows that List is immutable and function never needs recomposition because of it?

Only way i found is wrapping like @Immutable data class ImmutableList<T>(val items: List<T>). Demo (when Child1 recomposes Parent, Child2 with same List gets recomposed too):

class TestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBasicsTheme {
                Parent()
            }
        }
    }
}

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val items = remember { listOf(1, 2, 3) }

    Column {
        // click forces recomposition of Parent
        Child1(value = state.value,
            onClick = { state.value = !state.value })

        //
        Child2(items)
    }
}

@Composable
fun Child1(
    value: Boolean,
    onClick: () -> Unit
) {
    Log.d("Test", "Child1 Draw")
    Text(
        "Child1 ($value): Click to recompose Parent",
        modifier = Modifier
            .clickable { onClick() }
            .padding(8.dp)
    )
}

@Composable
fun Child2(items: List<Int>) {
    Log.d("Test", "Child2 Draw")
    Text(
        "Child 2 (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}
Convolute answered 10/9, 2021 at 19:38 Comment(7)
It should not be recomposed. Please provide a minimal reproducible example of your problem: what your composable with items argument looks like, how you call it from another composable and what causes top function recomposition.Nodical
@PhilipDukhov addedConvolute
It looks like you are doing everything right, it could be a bug. I suggest you report it in the compose issue tracker.Nodical
In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that(including reporting bugs like this one), but your code should work fine even if it is recomposed many times. With some animations, recomposition can happen around once a frame. Avoid doing any heavy calculations or changing view state directly in the view builder.Nodical
Maybe related: #68055378Convolute
@PhilipDukhov Correct me if I'm wrong but I don't think this is a bug. Even though the list is immutable, the elements inside could be mutable. In this case they're Int primitives, which happen to be immutable, but in general they can be anything so it doesn't make sense to me that you would annotate the list interface with StablePathognomy
@Pathognomy I expect Compose to compare the previous and the new collection hashes and if the hash haven't changed, it shouldn't recompose, no matter if the list is mutable or not. Here's a created issueNodical
R
6

You mainly have 2 options:

  1. Use a wrapper class annotated with either @Immutable or @Stable (as you already did).
  2. Compose compiler v1.2 added support for the Kotlinx Immutable Collections library.

With Option 2 you just replace List with ImmutableList. Compose treats the collection types from the library as truly immutable and thus will not trigger unnecessary recompositions.

Please note: At the time of writing this, the library is still in alpha.

I strongly recommend reading this article to get a good grasp on how compose handles stability (plus how to debug stability issues).

Ramey answered 27/11, 2022 at 0:26 Comment(0)
T
1

Another workaround is to pass around a SnapshotStateList.

Specifically, if you use backing values in your ViewModel as suggested in the Android codelabs, you have the same problem.

private val _myList = mutableStateListOf(1, 2, 3)
val myList: List<Int> = _myList

Composables that use myList are recomposed even if _myList is unchanged. Opt instead to pass the mutable list directly (of course, you should treat the list as read-only still, except now the compiler won't help you).

Example with also the wrapper immutable list:

@Immutable
data class ImmutableList<T>(
    val items: List<T>
)

var itemsList = listOf(1, 2, 3)
var itemsImmutable = ImmutableList(itemsList)

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val itemsMutableState = remember { mutableStateListOf(1, 2, 3) }

    Column {
        // click forces recomposition of Parent
        Child1(state.value, onClick = { state.value = !state.value })
        ChildList(itemsListState)                   // Recomposes every time
        ChildImmutableList(itemsImmutableListState) // Does not recompose
        ChildSnapshotStateList(itemsMutableState)   // Does not recompose
    }
}

@Composable
fun Child1(
    value: Boolean,
    onClick: () -> Unit
) {
    Text(
        "Child1 ($value): Click to recompose Parent",
        modifier = Modifier
            .clickable { onClick() }
            .padding(8.dp)
    )
}

@Composable
fun ChildList(items: List<Int>) {
    Log.d("Test", "List Draw")
    Text(
        "List (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

@Composable
fun ChildImmutableList(items: ImmutableList<Int>) {
    Log.d("Test", "ImmutableList Draw")
    Text(
        "ImmutableList (${items.items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

@Composable
fun ChildSnapshotStateList(items: SnapshotStateList<Int>) {
    Log.d("Test", "SnapshotStateList Draw")
    Text(
        "SnapshotStateList (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}
Trickery answered 26/9, 2022 at 14:20 Comment(0)
L
1

Because Compose have assumption when collection type such List, Set and Map class it's unstable parameter. so, add annotation it to @Stable() or @Immutable in your class.

@Immutable
data class Foo(val lorem: List<String>) 
Latonya answered 2/10, 2023 at 11:25 Comment(0)
T
0

Just to add to the answers regarding wrapping the collection in a wrapper class and marking as @Stable or @Immutable, you can add by to use delegation. Delegating to the wrapped collection can be handy if you want to be able to access the collection without directly referencing it:

/**
 * List wrapper for Composable performance optimization. Uses delegation for read only list operations.
 */
@Immutable
data class ImmutableList<T>(
    val wrapped: List<T> = listOf()
) : List<T> by wrapped

The wrapper can then be used as though it were the list itself:

val myList = ImmutableList<SomeClass>(listOf(...))
val firstValue = myList.first()
val lastValue = myList.last()
val size = myList.size
Tilney answered 21/5, 2024 at 2:57 Comment(0)
R
-2

Using lambda, you can do this

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val items = remember { listOf(1, 2, 3) }
    val getItems = remember(items) {
        {
            items
        }
    }

    Column {
        // click forces recomposition of Parent
        Child1(value = state.value,
            onClick = { state.value = !state.value })

        //
        Child2(items)
        Child3(getItems)
    }
}

@Composable
fun Child3(items: () -> List<Int>) {
    Log.d("Test", "Child3 Draw")
    Text(
        "Child 3 (${items().size})",
        modifier = Modifier
            .padding(8.dp)
    )
}
Reproduce answered 25/7, 2022 at 1:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.