compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

NullPointerException with LazyColumn (1.0.0-rc6)

Open Sydenth opened this issue 4 years ago • 3 comments

Tested on 1.0.0-rc6.

I'm not sure whether this is a bug or my (the developer's) fault:

fun BugRepro() = singleWindowApplication {
    var list: List<String> by remember { mutableStateOf(emptyList()) }
    val map: Map<String, String> = list.associateWith { it.uppercase() }.also { println("computed map") }

    Column {
        Button({ list = list + "text ${Random().nextInt()}" }) { Text("Add") }

        LazyColumn {
            items(list) { item ->
                println("rendering item")
                Thread.sleep(500)
                Text(map[item]!!, Modifier.size(400.dp, 50.dp).background(Color.Red))
            }
        }
    }
}

(Thread.sleep for demonstration only, I ran into this without it.)

When I click on the button, the item appears as expected. However, when I click the button and move my mouse to the expected item position quickly enough, the app crashes with a NullPointerException and a cryptic stack trace:

Stack Trace
rendering item
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at ComposableSingletons$BugKt$lambda-2$1$1$2$invoke$$inlined$items$default$2.invoke(LazyDsl.kt:265)
	at ComposableSingletons$BugKt$lambda-2$1$1$2$invoke$$inlined$items$default$2.invoke(LazyDsl.kt:99)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:135)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.foundation.lazy.LazyListScopeImpl$items$1$1.invoke(LazyListScopeImpl.kt:39)
	at androidx.compose.foundation.lazy.LazyListScopeImpl$items$1$1.invoke(LazyListScopeImpl.kt:39)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.runtime.saveable.SaveableStateHolderImpl.SaveableStateProvider(SaveableStateHolder.kt:84)
	at androidx.compose.foundation.lazy.layout.LazyLayoutItemContentFactory$CachedItemContent$content$1.invoke(LazyLayoutItemContentFactory.kt:102)
	at androidx.compose.foundation.lazy.layout.LazyLayoutItemContentFactory$CachedItemContent$content$1.invoke(LazyLayoutItemContentFactory.kt:93)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.layout.SubcomposeLayoutState$subcompose$2$1$1.invoke(SubcomposeLayout.kt:251)
	at androidx.compose.ui.layout.SubcomposeLayoutState$subcompose$2$1$1.invoke(SubcomposeLayout.kt:251)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:72)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2582)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2571)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:247)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2571)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime(Composer.kt:2522)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:478)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:748)
	at androidx.compose.runtime.ComposerImpl$CompositionContextImpl.composeInitial$runtime(Composer.kt:2987)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:433)
	at androidx.compose.ui.layout.SubcomposeLayoutState.subcomposeInto(SubcomposeLayout.kt:269)
	at androidx.compose.ui.layout.SubcomposeLayoutState.access$subcomposeInto(SubcomposeLayout.kt:154)
	at androidx.compose.ui.layout.SubcomposeLayoutState$subcompose$2.invoke(SubcomposeLayout.kt:244)
	at androidx.compose.ui.layout.SubcomposeLayoutState$subcompose$2.invoke(SubcomposeLayout.kt:241)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver.withNoObservations(SnapshotStateObserver.kt:142)
	at androidx.compose.ui.node.OwnerSnapshotObserver.withNoSnapshotReadObservation$ui(OwnerSnapshotObserver.kt:55)
	at androidx.compose.ui.node.LayoutNode.withNoSnapshotReadObservation$ui(LayoutNode.kt:1125)
	at androidx.compose.ui.layout.SubcomposeLayoutState.subcompose(SubcomposeLayout.kt:241)
	at androidx.compose.ui.layout.SubcomposeLayoutState.subcompose(SubcomposeLayout.kt:235)
	at androidx.compose.ui.layout.SubcomposeLayoutState.subcompose$ui(SubcomposeLayout.kt:224)
	at androidx.compose.ui.layout.SubcomposeLayoutState$Scope.subcompose(SubcomposeLayout.kt:490)
	at androidx.compose.foundation.lazy.layout.LazyLayoutPlaceablesProvider.getAndMeasure-0kLqBqw(LazyMeasurePolicy.kt:58)
	at androidx.compose.foundation.lazy.LazyMeasuredItemProvider.getAndMeasure-ZjPyQlc(LazyMeasuredItemProvider.kt:45)
	at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-DCyOChk(LazyListMeasure.kt:140)
	at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.measure-3p2s80s(LazyList.kt:230)
	at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2.invoke-0kLqBqw(LazyLayout.kt:55)
	at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2.invoke(LazyLayout.kt:44)
	at androidx.compose.ui.layout.SubcomposeLayoutState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:355)
	at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:51)
	at androidx.compose.foundation.layout.PaddingValuesModifier.measure-3p2s80s(Padding.kt:417)
	at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:306)
	at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:39)
	at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:118)
	at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:100)
	at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$3.invoke(OuterMeasurablePlaceable.kt:99)
	at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:1798)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:121)
	at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui(OwnerSnapshotObserver.kt:88)
	at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui(OwnerSnapshotObserver.kt:76)
	at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99)
	at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1254)
	at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui$default(LayoutNode.kt:1250)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure(MeasureAndLayoutDelegate.kt:174)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:232)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:38)
	at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:205)
	at androidx.compose.ui.platform.SkiaBasedOwner.measureAndLayout(SkiaBasedOwner.skiko.kt:266)
	at androidx.compose.ui.node.Owner$DefaultImpls.measureAndLayout$default(Owner.kt:182)
	at androidx.compose.ui.platform.SkiaBasedOwner.processPointerInput-gBdvCQM$ui(SkiaBasedOwner.skiko.kt:342)
	at androidx.compose.ui.platform.SkiaBasedOwner.processPointerInput-gBdvCQM$ui$default(SkiaBasedOwner.skiko.kt:341)
	at androidx.compose.ui.ComposeScene.onMouseMove(ComposeScene.skiko.kt:463)
	at androidx.compose.ui.ComposeScene.sendPointerEvent-Kr8mkKM(ComposeScene.skiko.kt:423)
	at androidx.compose.ui.ComposeScene.sendPointerEvent-Kr8mkKM$default(ComposeScene.skiko.kt:392)
	at androidx.compose.ui.awt.ComposeLayer_desktopKt.onMouseEvent(ComposeLayer.desktop.kt:359)
	at androidx.compose.ui.awt.ComposeLayer_desktopKt.access$onMouseEvent(ComposeLayer.desktop.kt:1)
	at androidx.compose.ui.awt.ComposeLayer$onMouseEvent$1.invoke(ComposeLayer.desktop.kt:283)
	at androidx.compose.ui.awt.ComposeLayer$onMouseEvent$1.invoke(ComposeLayer.desktop.kt:282)
	at androidx.compose.ui.awt.ComposeLayer.catchExceptions(ComposeLayer.desktop.kt:88)
	at androidx.compose.ui.awt.ComposeLayer.onMouseEvent(ComposeLayer.desktop.kt:282)
	at androidx.compose.ui.awt.ComposeLayer.access$onMouseEvent(ComposeLayer.desktop.kt:69)
	at androidx.compose.ui.awt.ComposeLayer$4.mouseMoved(ComposeLayer.desktop.kt:263)
	at java.desktop/java.awt.Component.processMouseMotionEvent(Component.java:6659)
	at java.desktop/java.awt.Component.processEvent(Component.java:6383)
	at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:4990)
	at java.desktop/java.awt.Component.dispatchEvent(Component.java:4822)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:772)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
	at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:745)
	at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:743)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
computed map

From the code above, I would expect that map[item]!! cannot fail. Hoewever, the issue is that the mouse triggers the item to be rendered before the map is recomputed (and then once more immediately after). The stack trace isn't exactly helpful here either because of the inlining of items. This does not happen when using derivedStateOf for the map but one probably wouldn't immedialety think this is necessary here since it usually works when not moving the mouse.

Is this a bug? If not, is there a way to better alert the developer as to why this is happening?

Sydenth avatar Nov 30 '21 08:11 Sydenth

Thanks for the report!

Video of reproducer:

https://user-images.githubusercontent.com/5963351/144014874-2c17bfa9-e886-4e4f-9c2b-028a1210bc58.mp4

igordmn avatar Nov 30 '21 08:11 igordmn

This is what happens:

  1. we click on the Button, change the list state
  2. move to the lazy list
  3. force remeasure lazy list items (don't know why we do it on move, but that is optimization details, it can also happen on click)
  4. force recompose lazy list items
  5. because we only recompose the item, and haven't recomposed the main composition, we have an NPE. the main recomposition will happen only on the next frame.

The issue here that we access captured state from the outer scope, that is not remembered across recompositions. Yes, it is always updated during the next recomposition, but we can encounter races as described above.

Another way to encounter a race - is to read map in some Move event listener. When Move event happens, items contains up-to-date data, but map still contains the old data, because there was no recomposition. In that case we also encounter an NPE.

It is probably never right to access "non-wrapped" state from the outer scope, and we need to somehow add an IDE inspection for that.

igordmn avatar Nov 30 '21 09:11 igordmn

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

okushnikov avatar Aug 26 '24 16:08 okushnikov