11

@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)
    )
}
z.g.y
  • 5,512
  • 4
  • 10
  • 36
Jemshit
  • 9,501
  • 5
  • 69
  • 106
  • It should not be recomposed. Please provide a [minimal reproducible example](https://stackoverflow.com/help/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. – Phil Dukhov Sep 11 '21 at 05:03
  • @PhilipDukhov added – Jemshit Sep 11 '21 at 06:05
  • It looks like you are doing everything right, it could be a bug. I suggest you [report](https://issuetracker.google.com/issues/new?component=612128) it in the compose issue tracker. – Phil Dukhov Sep 11 '21 at 06:35
  • 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. – Phil Dukhov Sep 11 '21 at 06:39
  • Maybe related: https://stackoverflow.com/questions/68054878/how-to-favor-smart-recomposition-in-jetpack-compose – Jemshit Sep 12 '21 at 14:00
  • @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 Stable – kng Nov 20 '21 at 19:01
  • @kng 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 issue](https://issuetracker.google.com/issues/199496149) – Phil Dukhov Nov 20 '21 at 22:55

3 Answers3

3

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).

Hanan Rofe Haim
  • 870
  • 6
  • 11
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)
    )
}
rileyx
  • 322
  • 4
  • 11
-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)
    )
}