Kotlin / kotlinx.collections.immutable

Immutable persistent collections for Kotlin

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

persistentListOf & compose compiler issue

Skyyo opened this issue · comments

Not sure if this is Compose Compiler or this collections library issue, so crossposting in both places. Link to google issue tracker - https://issuetracker.google.com/issues/254435410

Problem:
ImmutableList is considered stable by the Compose Compiler, but creating a list of integers using persistentListOf(0, 1, 2) will cause recompositions.
Creating list using listOf(0, 1, 2).toImmutableList() won't.

Steps to reproduce:

  1. Add "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" dependency;
  2. Paste code below:
@Composable
fun TestScreen() {
    var count by remember { mutableStateOf(0) }
    val listViaPersistentListBuilder = remember { persistentListOf(1, 2, 3) }
    val listViaCasting = remember { listOf(1, 2, 3).toImmutableList() }

    Column {
        Texts(listViaPersistentListBuilder) // will cause recompositions
        Texts(listViaPersistentListBuilder as ImmutableList<Int>)  // casting fixes the issue
        Texts(listViaCasting) // works as expected
        Button(modifier = Modifier.size(108.dp), onClick = { count++ }) { Text(text = "count++") }
        CounterText(count)
    }
}

@Composable
fun Texts(items: ImmutableList<Int>) {
    for (item in items) {
        Text(text = "Item $item ")
    }
}

@Composable
fun CounterText(count: Int) {
    Text(text = "Count: $count")
}
  1. Click on "count++" button to cause recompositions;
  2. Use layout inspector tool to track the recompositions.

or use this repository

UPDATE:
google issue tracker got answer:
We should consider adding PersistentList and friends to this list or considering any interface that derives from any of the above also stable.

Is it possible to consider immutableListOf() builder since it matters for Compose?

We've addressed this in the Compose compiler. All of the Persistent and Immutable collections in kotlinx.collections.immutable should be treated as inherently stable by the Compose compiler, including the *Of() builders.

We're maintaining these as a list of classes and functions that are assumed to be stable (rather than relying static analysis of Kotlin's collection types), so if the signatures change or if a new collection is added, we might need to update our list again. But this shouldn't require any changes to the kotlinx artifact.

Hi @andrewbailey I'm facing the same issue and I'm using the Compose Compiler Version 1.5.0 which is almost latest
Here is my data class

@Stable
data class LaunchesScreenState(
    val launches: ImmutableList<LaunchDetail> = persistentListOf(),
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

The Output of Compose Compiler Metric report for the above class is

stable class LaunchesScreenState {
  runtime val reminders: ImmutableList<LaunchDetail>
  stable val isLoading: Boolean
  stable val infoMessage: String?
}

The Compose compiler cannot infer that variable reminders is stable even though I have used ImmutableList and the Reminder class is considered stable. Does runtime val means that the stability will be inferred at runtime?

Runtime stability indicates that the stability can't be guaranteed at compile-time because the type isn't marked as @Stable and isn't one of the known-stable types. In other words, the type ImmutableList<LaunchDetail> isn't stable as part of its API contract. The type could still be stable, though, as inferred by the Compose compiler. That can change out from underneath us if the type is in a different compilation unit that changes underneath us without you recompiling your project (e.g. if you're a library and need to know the stability of a class in another library), which is why we wait until runtime to determine stability.

Before this change, ImmutableList would have been treated as unstable, because it's an interface. But type parameters also influence the stability of a type. So ImmutableList<LaunchDetail> can only be stable if both the ImmutableList and LaunchDetail are stable. What's likely happening is that LaunchDetail isn't explicitly marked as Stable, so Compose will wait to resolve its stability until runtime. If that's the case, you can ignore this so long as LaunchDetail is in fact being inferred as stable.

If LaunchDetail is expected to be a known stable type (i.e. it's marked with @Stable), then ImmutableList<LaunchDetail> should be inferred as stable and this is likely an issue either in the reporting or in our stability inferencing. But if LaunchDetail is getting the stability inferencing from the Compose compiler, then this seems to be working as expected.

Thanks for the info @andrewbailey
Can I say that even if LaunchDetail is being considered Stable in the Compiler Metrics Report but it isn't marked with the @Stable annotation then the variable
val launches: ImmutableList<LaunchDetail> = persistentListOf()
still wouldn't be considered Stable at compile time?

It depends on how LaunchDetail is defined. A general overview for the stability inferencing rules looks something like this:

  • If the type is annotated with @Stable, it's treated as known stable in its own and all other compilation modules
  • Otherwise, if the type was compiled in a module that doesn't compile with the Compose compiler, it will be treated by other compilation modules as being known unstable.
  • Otherwise, if the type is an interface, it's treated as unstable
  • Otherwise, the type is given a runtime stability field. Its stability cannot be known at compile-time and we only resolve it when the program is running. The value of this stability field is determined by the aggregate of all its type parameter and property stabilities
    • If any type that composes the analyzed type is known unstable or mutable, then the runtime stability field will be compiled as unstable. Within the compilation module that the type is declared, we'll assume known unstable. For other compilation modules, we need to check the stability field at runtime — even though that class wasn't stable at the time, it might become stable in the future.
    • If all of its types are known stable, the stability of the class we're inferencing for is stable. Inside that compilation module, we'll treat it as a known stable type. Outside of that compilation module, it's treated as runtime stable for similar reasons as before. It might become unstable in the future.
    • If there's a mix of runtime stable and known stable types (with no known unstable types), it becomes runtime stable everywhere. We need to check at runtime whether all the runtime stable types are stable.

It's very likely that this is working as intended and your stability is being inferred correctly. I also don't know 100% for certain how that report output works compared to what the compiler sees, so the stability report may also just be showing an unexpected output.

If you're still unsure, you can consider debugging your program and see if you're observing more recompositions than you're expecting. If you can reproduce a problem where we're inferring stability incorrectly, please file a separate issue.

I see, thanks for the detailed explanation. I checked and there were no unnecessary recompositions. The LaunchDetail class is defined in my domain module which is different from module in which LaunchesScreenState resides (app module) but again I enabled the Compose compiler in the domain module (it is a Java/Kotlin library module) for report generation purposes and reverted the changes back before pushing the changes.
I'll keep in mind the details you mentioned if I see unnecessary recompositions in the future. Thanks

Great to see the issue resolved.