Search code examples
androidkotlinandroid-studionavigationandroid-jetpack-compose

Android Compose Constraint Layout in a LazyColumn, in a NavHost: error - replace() called on item that was not placed


I have a large project where i was getting this exception - replace() called on item that was not placed. I was able to narrow the root of the problem to having a Constraint Layout within a NavHost. I created an extremely simplified project where it is reproducible. I emphasize simplified, bc the consraintlayout is not even used properly here, constraints not applied - that's on purpose, to simplify and show that it doesnt matter in this case (in the real project, all constraints in place) Github link to the simplified project here Also, relevant code snippets below. If I call the MainContent() directly, no issues. If i call thru NavHost, i get a crash when i scroll thru the lazycolumn.

Questions

  1. Is there anything wrong with the code?
  2. If this is a Compose bug, I tried with many different versions of libraries - it must have been there for a long time. I saw a couple of stackoveflow questions on this, but none of them related the crash to navhost - is navhost just one of the conditions where the bug manifests itself?

java.lang.IllegalStateException: replace() called on item that was not placed at androidx.compose.ui.node.LayoutNodeLayoutDelegate$LookaheadPassDelegate.replace(LayoutNodeLayoutDelegate.kt:1569) at androidx.compose.ui.node.LayoutNode.lookaheadReplace$ui_release(LayoutNode.kt:923) at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded

enter image description here

and specifically this is the code in Layoutnode.kt from Compose:

internal fun lookaheadReplace() {
        if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
            // This LayoutNode may have asked children for intrinsics. If so, we should
            // clear the intrinsics usage for everything that was requested previously.
            clearSubtreePlacementIntrinsicsUsage()
        }
        **lookaheadPassDelegate!!.replace()**
    }

Activity:

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
           // MainContent() -- this works
            val navController = rememberNavController()
            // with NavHost, it crashes.
            NavHost(
                navController,
                startDestination =  NavRoutes.MainRoute.name
            ) {
                mainGraph()
            }
        }
    }
}

Main Graph:

fun NavGraphBuilder.mainGraph(
) {
    navigation(startDestination = MainNavOption.Main.name, route = NavRoutes.MainRoute.name) {
        composable(MainNavOption.Main.name){
            MainContent()
        }
    }
}

Main Content:

@Composable
fun MainContent() {
    WhyConstraintTheme {
        val listState = rememberLazyListState()
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            var listItems = mutableListOf<ElementHolder>()
            for (i in 1.rangeTo(149)) {
                listItems.add(ElementHolder(i.toString()))
            }
            BoxWithConstraints(Modifier.fillMaxSize()) {
                LazyColumn(
                    state = listState,
                    contentPadding = PaddingValues(1.dp),
                    verticalArrangement = Arrangement.Center
                ) {
                    items(listItems) { item: ElementHolder ->
                        ElementListItem(
                            element = item.itemDescription,
                        )
                    }
                }
            }
        }
    }
}

Element H

@Composable fun ElementListItem( element: String, ) {

ConstraintLayout() {
    val (
        divider, year, description, image,
        overflow
    ) = createRefs()

    Text(
        text = "element:  {$element}",
        modifier = Modifier.constrainAs(year) {
            linkTo(
                start = parent.start,
                end = parent.end,
            )
        }
    )
}

}

Versions used (btw, tried with many different versions, going back to a number of previous versions)

val material_version = "1.5.4" implementation ("androidx.compose.material:material:$material_version")

implementation ("androidx.compose.material:material-icons-extended:$material_version")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.1")
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation ("androidx.constraintlayout:constraintlayout-compose:1.0.1")

implementation ("androidx.navigation:navigation-compose:2.7.5")

EDIT: Another trigger for crash is AnimatedContent. If you wrap the LazyColumn with a constraint layout inside an AnimatedContent (instead of NavHost), same crash happens. So probably the ultimate root cause has something to do with animations inside both NavHost and AnimatedContent - a Composable inside NavHost has AnimatedContentScope.


Solution

  • A fix for the crash seems to be upgrading the ConstraintLayout from 1.0.1 to 1.1.0-alpha13.