slackhq / circuit

⚡️ A Compose-driven architecture for Kotlin and Android applications.
https://slackhq.github.io/circuit/
Apache License 2.0
1.52k stars 76 forks source link

NavDecorations cannot be declared outside of module where they are used #1778

Closed matthewbahr-clear closed 1 week ago

matthewbahr-clear commented 2 weeks ago

Circuit Version 0.24.0

Bit of a strange one here but I've encountered a hard crash when attempting to modularize a NavDecoration into a separate package from the UI layer that is implementing it. I have a core utility package that is used by any circuit UI module that has standard utilities for the UI. We just added in a standard NavDecorator based on the default that does a slide in/out transition.

The weirdness happens when you take the NavDecorator out of the same module as the NavigableCircuitContent where it is used. If you do so you'll end up with a abstract method crash at runtime when the NavigableCircuitContent is first invoked.

java.lang.AbstractMethodError: abstract method "void com.slack.circuit.backstack.NavDecoration.DecoratedContent(kotlinx.collections.immutable.ImmutableList, int, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int)"

Workarounds I've tried (mostly desperate flailing):

  1. Making it into a class rather than an object
  2. Making the object into a return value from a function instead of directly referencing the object
  3. Making an inline function that creates the object
  4. Moving packages around inside the same module (nothing affects it)
  5. Delegation to it by another class in the same module

So far nothing works: It will always fail unless I've moved it into the same module as the view, meaning that I can never reuse the code elsewhere.

TLDR:

If the Decorator is in a different module the application will crash at runtime when the NavigableCircuitContent is first invoked.

If the Decorator is in the same module (even if it's a different package in the same module) it works fine.

Stacktrace:

Full stack trace of AbstractMethodError ``` java.lang.AbstractMethodError: abstract method "void com.slack.circuit.backstack.NavDecoration.DecoratedContent(kotlinx.collections.immutable.ImmutableList, int, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int)" at com.slack.circuit.foundation.NavigableCircuitContentKt$NavigableCircuitContent$2.invoke(NavigableCircuitContent.kt:110) at com.slack.circuit.foundation.NavigableCircuitContentKt$NavigableCircuitContent$2.invoke(NavigableCircuitContent.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248) at com.slack.circuit.foundation.NavigableCircuitContentKt.NavigableCircuitContent(NavigableCircuitContent.kt:109) at (Fragment wrapping the view) at (Fragment wrapping the view) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at com.slack.circuit.foundation.CircuitCompositionLocalsKt$CircuitCompositionLocals$1.invoke(CircuitCompositionLocals.kt:31) at com.slack.circuit.foundation.CircuitCompositionLocalsKt$CircuitCompositionLocals$1.invoke(CircuitCompositionLocals.kt:30) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at com.slack.circuit.foundation.CircuitCompositionLocalsKt.CircuitCompositionLocals(CircuitCompositionLocals.kt:27) at (Fragment wrapping the view) at (Fragment wrapping the view)$onViewCreated$1$1.invoke((Fragment wrapping the view)) at (Fragment wrapping the view)$onViewCreated$1$1.invoke((Fragment wrapping the view)) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:428) at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:252) at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:251) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:186) at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:119) at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:118) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:110) 14:48:33.433 E at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:139) at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:138) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248) at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:138) at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:123) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:90) at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3302) at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3235) at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:725) at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:1071) at androidx.compose.runtime.CompositionImpl.composeInitial(Composition.kt:633) at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:619) at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:123) at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:114) at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1289) at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:114) at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:164) at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.jvm.kt:320) at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.jvm.kt:198) at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:121) at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:114) at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1364) at android.view.View.dispatchAttachedToWindow(View.java:23244) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3722) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3729) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3958) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:3345) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:11437) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1690) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1699) at android.view.Choreographer.doCallbacks(Choreographer.java:1154) at android.view.Choreographer.doFrame(Choreographer.java:1080) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1647) at android.os.Handler.handleCallback(Handler.java:958) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:230) at android.os.Looper.loop(Looper.java:319) at android.app.ActivityThread.main(ActivityThread.java:9063) 14:48:33.433 E at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:588) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103) ```

Problematic Code

Full code of the Decoration that works only inside the same module Exact code of the Slider here: (Duplicate of `DefaultDecoration.kt` with the 10% increased to 100% and the fade removed) ```kt object SlideInOutNavDecoration : NavDecoration { private val FastOutExtraSlowInEasing = CubicBezierEasing(0.208333f, 0.82f, 0.25f, 1f) private const val NORMAL_DURATION = 450 private val forward: ContentTransform by lazy { computeTransition(1) } private val backward: ContentTransform by lazy { computeTransition(-1) } private fun computeTransition(sign: Int): ContentTransform { val enterTransition = slideInHorizontally( initialOffsetX = { fullWidth -> fullWidth * sign }, animationSpec = tween(durationMillis = NORMAL_DURATION, easing = FastOutExtraSlowInEasing), ) + expandHorizontally( animationSpec = tween(durationMillis = NORMAL_DURATION, easing = FastOutExtraSlowInEasing), initialWidth = { (it * .9f).toInt() }, expandFrom = if (sign > 0) Alignment.Start else Alignment.End, ) val exitTransition = slideOutHorizontally( targetOffsetX = { fullWidth -> fullWidth * -sign }, animationSpec = tween(durationMillis = NORMAL_DURATION, easing = FastOutExtraSlowInEasing), ) + shrinkHorizontally( animationSpec = tween(durationMillis = NORMAL_DURATION, easing = FastOutExtraSlowInEasing), targetWidth = { (it * .9f).toInt() }, shrinkTowards = Alignment.End, ) return enterTransition togetherWith exitTransition } @Composable override fun DecoratedContent( args: ImmutableList, backStackDepth: Int, modifier: Modifier, content: @Composable (T) -> Unit, ) { AnimatedContent( targetState = args, modifier = modifier, transitionSpec = { // A transitionSpec should only use values passed into the `AnimatedContent`, to // minimize // the transitionSpec recomposing. The states are available as `targetState` and // `initialState` val diff = targetState.size - initialState.size val sameRoot = targetState.lastOrNull() == initialState.lastOrNull() when { sameRoot && diff > 0 -> forward sameRoot && diff < 0 -> backward else -> fadeIn() togetherWith fadeOut() }.using( // Disable clipping since the slide-in/out should // be displayed out of bounds. SizeTransform(clip = true), ) }, label = "SlideInOutNavDecoration", ) { content(it.first()) } } } ```
stagg commented 2 weeks ago

This looks a lot like the compose compiler isn't run in the module that SlideInOutNavDecoration is getting moved to 🤔

matthewbahr-clear commented 2 weeks ago

That could explain it!

The module that the decoration is moved into has all the standard circuit dependencies:

    // Add circuit dependencies
    implementation libs.circuit.foundation
    implementation libs.circuit.overlay
    implementation libs.circuitx.overlays
    implementation libs.circuitx.effects
    api libs.circuit.codegen.annotations
    ksp libs.circuit.codegen.core

    testImplementation libs.circuit.test

Does it need to also have androidx.compose.foundation to work? Or androidx.compose.ui?

stagg commented 2 weeks ago

It'll also need the compose gradle plugin applied, then the overridden @Composable function will get fully generated 🎉

matthewbahr-clear commented 1 week ago

Yep that was it. Missed the compose gradle plugin application!

Thanks!