question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Router.push is ignored when in the process of transitioning to another component.

See original GitHub issue

I’ve found a bug where a router push event is ignored when performed during an ongoing navigation.

I haven’t migrated to the latest Kotlin yet, so I’m still on Decompose 0.1.7, and Kotlin 1.4.21-2

Here is the situation: I have a RootComponent, which has 3 child components, A, B, and C.

We start the app by displaying A where an auth occurs

router.navigate { listOf(Screen.A) }

After the user logs in, we navigate to Screen B, and remove A from the backstack:

router.navigate { listOf(Screen.B) }

Inside of Component B, I was trying to debug something and so I called

    init {
        lifecycle.doOnResume {
            resumePauseLaunch { loadData() }
            output(Output.ActionThatNavigatesToC)
        }
    }

Which ultimately triggers this in the root component:

router.push(Screen.C)

Even though C is the last screen pushed to, the View isn’t updated and it just stays on Screen B. Calling output(Output.ActionThatNavigatesToC) after the screen transition to B has completed, works as intended.

Message me on Slack, and I can step you through the code if you’d like more details.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
arkivanovcommented, Feb 12, 2021

Thanks! This looks like the same issue. Component C is being pushed recursively. I will fix.

0reactions
ScottPiercecommented, Feb 12, 2021

I’ve accurately reproduced it here:

MainActivity:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

        setContent {
            ComposeAppTheme {
                Surface(color = Color.White) {
                    rootComponent(::Root).model.rootComposable()
                }
            }
        }
    }

    @Composable
    fun Root.Model.rootComposable() {
        Children(child, animation = crossfade()) { child, config ->
            when (config) {
                is Root.Config.A -> {
                    println("Showing A")
                    Text("A", style = MaterialTheme.typography.h3)
                }
                is Root.Config.B -> {
                    println("Showing B")
                    Text("B", style = MaterialTheme.typography.h3)
                }
                is Root.Config.C -> {
                    println("Showing C")
                    Text("C", style = MaterialTheme.typography.h3)
                }
            }
        }
    }
}

private fun <T, K> crossfade(): @Composable (currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit) -> Unit =
    { currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit ->
        KeyedCrossfade(currentChild, currentKey, children)
    }

@Composable
private fun <T, K> KeyedCrossfade(currentChild: T, currentKey: K, children: @Composable (T, K) -> Unit) {
    androidx.compose.animation.Crossfade(
        current = ChildWrapper(currentChild, currentKey),
        animation = tween(1000)
    ) {
        children(it.child, it.key)
    }
}

private class ChildWrapper<out T, out C>(val child: T, val key: C) {
    override fun equals(other: Any?): Boolean = key == (other as? ChildWrapper<*, *>)?.key
    override fun hashCode(): Int = key.hashCode()
}
class ChildA(
    context: ComponentContext,
    next: () -> Unit,
) : ComponentContext by context {
    init {
        lifecycle.doOnResume {
            GlobalScope.launch(Dispatchers.Main) {
                next()
            }
        }
    }
}

class ChildB(
    context: ComponentContext,
    onFinished: () -> Unit
) : ComponentContext by context {
    init {
        lifecycle.doOnResume {
            onFinished()
        }
    }
}

class ChildC

class Root(context: ComponentContext) : ComponentContext by context {
    interface Model {
        val child: Value<RouterState<Config, Any>>
    }

    private val router =
        router<Config, Any>(
            initialConfiguration = Config.A,
            componentFactory = ::createChild,
            handleBackButton = true
        )

    val model = object : Model {
        override val child: Value<RouterState<Config, Any>> = router.state
    }

    private fun createChild(config: Config, context: ComponentContext): Any =
        when (config) {
            is Config.A -> ChildA(context) {
                println("Start Nav B")
                router.navigate { listOf(Config.B) }
                println("End Nav B")
            }

            is Config.B ->
                ChildB(context) {
                    println("Start Nav C")
                    router.push(Config.C)
                    val c = router.state.value.activeChild.configuration
                    println("End Nav C: $c")
                }

            is Config.C -> ChildC()
        }

    sealed class Config : Parcelable {
        @Parcelize
        object A : Config()

        @Parcelize
        object B : Config()

        @Parcelize
        object C : Config()
    }
}

Here is the Output:

System.out: Start Nav B
System.out: Start Nav C
System.out: End Nav C: com.example.Root$Config$C@658dadc
System.out: End Nav B
System.out: Showing A
System.out: Showing B

Notice how we end by showing B, instead of C which is the intended behavior.

Read more comments on GitHub >

github_iconTop Results From Across the Web

$router.push() followed by redirect in a route's beforeEnter ...
It's simply a navigation ( $router. push ) that generates a redirect ( next('/foo') ) in a beforeEnter hook.
Read more >
Vue.$router.push with same route but different parameters
My problem is, that since Vue doesn't reload the component, the transition effect is not triggered.
Read more >
Waiting for the result of a Navigation - Vue Router
When using router-link , Vue Router calls router.push to trigger a navigation. While the expected behavior for most links is to navigate a...
Read more >
Using Vue 3's Router in Practice - Daily.dev
Params will be ignored if the path is used. Therefore you need to use name instead of the path if you are passing...
Read more >
Complete Vue Router 4 Guide: Basics, Programmatic Routing ...
RouterView is another built-in component. ... The createRouter method is what we use to create a new Vue Router instance.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found