Hey everyone, I'm using the <Android Dev Guide> to...
# compose
s
Hey everyone, I'm using the Android Dev Guide to build a custom modifier. It's basically the
placeholder
modifier from Accompanist, but updated for the new
Modifier.node
system since Accompanist has deprecated theirs. However, I'm sometimes getting a crash because the coroutineScope couldn't be created. There's not terribly much documentation about this new system, so I'm unsure how to tackle this. Code and some details are in the reply đź§µ
Here's a sample of what I'm doing.
coroutineScope
sometimes crashes, as described. I'm calling it from two places:
Node.update()
and
ModifierNodeElement.onAttach()
. The first one doesn't explicitly say that calling
coroutineScope
is allowed, but at this point the node should be attached to a layout and thus have an owner. The second one explicitly says that calling
coroutineScope
is allowed.
Copy code
private class MyNode(
    var foo: Boolean
) : Modifier.Node() {
    fun update() {
        // This calls requireOwner().coroutineContext
        // which rarely crashes with java.lang.IllegalStateException: This node does not have an owner.
        coroutineScope.launch { /* do stuff */ }
    }

    /**
     * Documentation of onAttach reads:
     * When called, `node` is guaranteed to be non-null. You can call sideEffect,
     * coroutineScope, etc.
     */
    override fun onAttach() {
        update()
    }
}

private data class MyElement(
    val foo: Boolean
) : ModifierNodeElement<MyNode>() {
    override fun create() = MyNode(foo)

    /**
     * Documentation of update reads:
     * Called when a modifier is applied to a Layout whose inputs have changed
     */
    override fun update(node: MyNode) {
        node.foo = foo
        node.update()
    }
}
The reason why I need to call coroutineScope in both cases is because the change in inputs (
foo
in this example) may require discarding the old coroutine and starting a new one. (As I said, the modifier is
placeholder
, so think of an input like
animationDuration
changing, which requires starting a new coroutine with
animatable.animateTo(..., animationDuration)
)
l
What version are you using? There was an issue before where update could be called while unattached, but this should be fixed in the latest beta
s
@Louis Pullen-Freilich [G] 1.7-alpha-08. That's one before latest beta. So that might actually be it. @Stylianos Gakis Thanks, I'll take a look! I searched around before I started implementing my own, but I didn't find any.
l
hm I think 1.7.0-alpha08 should also contain the fix
If you have a reproduction / full stack trace it would be helpful to file a bug for that
It’s a bit suspicious in any case to do this kind of coroutine work inside update(), but it probably shouldn’t crash with that error
âž• 1
s
I don't have the means to repro it right now, but here's the stack trace collected by my colleague
Copy code
Process: <http://com.example.app|com.example.app>, PID: 2006
java.lang.IllegalStateException: This node does not have an owner.
	at androidx.compose.ui.internal.InlineClassHelperKt.throwIllegalStateExceptionForNullCheck(InlineClassHelper.kt:30)
	at androidx.compose.ui.node.DelegatableNodeKt.requireOwner(DelegatableNode.kt:1291)
	at androidx.compose.ui.Modifier$Node.getCoroutineScope(Modifier.kt:207)
	at com.example.app.shimmer.ShimmerNode.update(Shimmer.kt:178)
	at com.example.app.shimmer.ShimmerElement.update(Shimmer.kt:76)
	at com.example.app.shimmer.ShimmerElement.update(Shimmer.kt:62)
	at androidx.compose.ui.node.NodeChainKt.updateUnsafe(NodeChain.kt:830)
	at androidx.compose.ui.node.NodeChainKt.access$updateUnsafe(NodeChain.kt:1)
	at androidx.compose.ui.node.NodeChain.updateNode(NodeChain.kt:672)
	at androidx.compose.ui.node.NodeChain.updateFrom$ui_release(NodeChain.kt:143)
	at androidx.compose.ui.node.LayoutNode.setModifier(LayoutNode.kt:893)
	at androidx.compose.ui.node.ComposeUiNode$Companion$SetModifier$1.invoke(ComposeUiNode.kt:47)
	at androidx.compose.ui.node.ComposeUiNode$Companion$SetModifier$1.invoke(ComposeUiNode.kt:47)
	at androidx.compose.runtime.changelist.Operation$UpdateNode.execute(Operation.kt:449)
	at androidx.compose.runtime.changelist.Operations.executeAndFlushAllPendingOperations(Operations.kt:309)
	at androidx.compose.runtime.changelist.ChangeList.executeAndFlushAllPendingChanges(ChangeList.kt:81)
	at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:984)
	at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:1013)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:678)
	at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:578)
	at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:41)
	at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
	at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
	at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1035)
	at android.view.Choreographer.doCallbacks(Choreographer.java:845)
	at android.view.Choreographer.doFrame(Choreographer.java:775)
	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1022)
	at android.os.Handler.handleCallback(Handler.java:938)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loopOnce(Looper.java:201)
	at android.os.Looper.loop(Looper.java:288)
	at android.app.ActivityThread.main(ActivityThread.java:7839)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@45acfef, androidx.compose.ui.platform.MotionDurationScaleImpl@7df8afc, StandaloneCoroutine{Cancelling}@16f2585, AndroidUiDispatcher@39fafda]