https://kotlinlang.org logo
Title
s

Steffen Funke

11/04/2021, 6:56 AM
I don’t seem to get
@Immutable
to work correctly for me - why do my list items recompose, when changing only a
isLoading
flag of my UI-State? 👉 Code in 🧵
typealias Items = List<String>

// ViewModel
class RecomposeDemoViewModel(repo: DummyRepo = DummyRepo()) {

    @Immutable
    data class UiState(
        val items: Items = emptyList(),
        val isLoading: Boolean = false,
        val error: Error? = null
    )

    var uiState by mutableStateOf(
        UiState(items = repo.sampleData)
    )

    fun changeSomeUiState() {
        uiState = uiState.copy(isLoading = !uiState.isLoading)
    }
}

// My Root View

@Composable
fun RecomposeDemo() {
    val viewModel by remember { mutableStateOf(RecomposeDemoViewModel()) }

    LogCompositions("Root")

    Column {
        Text("This demo uses a monolithic state data class.")

        val uiState = viewModel.uiState

        // Header
        Row {
            if (uiState.isLoading) {
                CircularProgressIndicator()
            }

            Button(onClick = viewModel::changeSomeUiState) {
                Text(text = "Toggle Loading State")
            }
        }


        // Why do list items in this LazyColumn recompose??
        MyColumn(items = viewModel.uiState.items)
    }
}

// Custom Lazycolumn to "narrow" recomposition scope -premature optimization?
@Composable
fun MyColumn(
    items: Items,
    modifier: Modifier = Modifier
) {
    LogCompositions("\tMy LazyColumn")

    LazyColumn(
        modifier = modifier.fillMaxWidth()
    ) {
        items(items) { item ->
            LogCompositions("\t\t${item}")

            Text(item, modifier = Modifier.padding(horizontal = 16.dp, vertical = 36.dp))
            Divider()
        }
    }
}
This picks up on this Twitter discussion: https://twitter.com/JimSproch/status/1456008752159621120 I don’t think the list items should recompose at all, when changing only a portion of the UI State? What am I missing here? Btw., I am logging recompositions with the fine
LogCompositions
composable, taken from this blog: https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose
Output on each Button click (which only changes
isLoading
-
items
stay the same):
Root >> Compositions:  1
	My LazyColumn >> Compositions:  1
		Item 0 >> Compositions:  1
		Item 1 >> Compositions:  1
		Item 2 >> Compositions:  1
		Item 3 >> Compositions:  1
		Item 4 >> Compositions:  1
		Item 5 >> Compositions:  1
		Item 6 >> Compositions:  1
		Item 7 >> Compositions:  1
>> Button click
Root >> Compositions:  2
	My LazyColumn >> Compositions:  2
		Item 0 >> Compositions:  2
		Item 1 >> Compositions:  2
		Item 2 >> Compositions:  2
		Item 3 >> Compositions:  2
		Item 4 >> Compositions:  2
		Item 5 >> Compositions:  2
		Item 6 >> Compositions:  2
		Item 7 >> Compositions:  2
...
a

Albert Chang

11/04/2021, 7:21 AM
You are probably misunderstanding
@Immutable
. If the class of a composable function parameter is marked with
@Immutable
(or
@Stable
) and it
equals()
the previous parameter, then the function can be skipped. You are not using
UiState
as the parameter of any composable functions and you are changing it at the first place, so its stability is irrelevant here. Recomposition is because
Items
is not stable.
s

Steffen Funke

11/04/2021, 7:29 AM
Yes, but I have also, while debugging, annotated
Items
(which btw. is only a typealias to
List<String>
should have made it clear) with
@Stable
or
@Immutable
, and no change. There is an ongoing discussion in the Twitter Thread about this experiences. Probably we are something missing. Basically it boils down to - why does the list recompose, if the inputs have not changed?
a

Albert Chang

11/04/2021, 7:32 AM
I don’t think marking a typealias has any effect. You have to create a wrapper class if you want to mark the list as stable.
s

Steffen Funke

11/04/2021, 7:35 AM
Hm, let me try that.
I think I read in some conversation that we can / should explicitely mark a
val
of type
List<T>
as
@Immutable
, for that reason. Which has not had any effect.
a

Albert Chang

11/04/2021, 7:42 AM
s

Steffen Funke

11/04/2021, 7:48 AM
@Albert Chang Wrapping the items into an
@Immutable data class
works indeed:
@Immutable
data class Items(
    val items: List<String>
)

...
class RecomposeDemoViewModel(val repo: DummyRepo = DummyRepo()) {

    data class UiState(
        val items: Items = Items(emptyList()),
        val isLoading: Boolean = false,
        val error: Error? = null
    )

    var uiState by mutableStateOf(
        UiState(items = Items(repo.sampleData))
    )
...
Now I only get the recompositions I expect.
Root >> Compositions:  0
	My LazyColumn >> Compositions:  0
		Item 0 >> Compositions:  0
		Item 1 >> Compositions:  0
		Item 2 >> Compositions:  0
		Item 3 >> Compositions:  0
		Item 4 >> Compositions:  0
		Item 5 >> Compositions:  0
		Item 6 >> Compositions:  0
		Item 7 >> Compositions:  0
>> Button Click
Root >> Compositions:  1
>> Button Click
Root >> Compositions:  2
>> Button Click
Root >> Compositions:  3
Yay! Thank you @Albert Chang
👍 1
For anyone interested, I have created a public working Compose Desktop gist here: https://gist.github.com/sfunke/b9f9cdcd8f02429819ec7d39e1d514bd Still not sure if it is intented behaviour, or if
List<T>
should not behave immutable as well, wenn annotated with
@Immutable
a

Albert Chang

11/04/2021, 8:00 AM
List<T>
is not immutable by nature, it is read-only. And you can’t annotate a class which is not in your control (typealias won’t work because it’s not a class).
s

Steffen Funke

11/04/2021, 8:11 AM
Found the related threads with discussions. So I can think we can close this here :thank-you: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1622465932419200 https://kotlinlang.slack.com/archives/CJLTWPH7S/p1631639793248900
o

Orhan Tozan

11/04/2021, 9:39 AM
@Albert Chang what about kotlinx.immutable.ImmutableList?
z

Zach Klippenstein (he/him) [MOD]

11/04/2021, 4:22 PM
You could also use SnapshotStateList, which is mutable but also stable (and uses the immutable list that Orphan suggested under the hood)