raamcosta / compose-destinations

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate.
https://composedestinations.rafaelcosta.xyz
Apache License 2.0
3.22k stars 133 forks source link

Question: Minimal navigation example #18

Closed StephanSchuster closed 2 years ago

StephanSchuster commented 3 years ago

What is the minimal and cleanest way to make the following navigation between three screens working:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Scaffold() {
    Column(modifier = Modifier.fillMaxSize()) {

        DestinationsNavHost(modifier = Modifier.fillMaxWidth().weight(1f))

        Row(
            modifier = Modifier.fillMaxWidth().wrapContentHeight(),
            horizontalArrangement = Arrangement.spacedBy(24.dp)
        ) {
            Button(onClick = { DestinationsNavigator.navigate(Screen1Destination) }) {
                Text("Screen 1")
            }
            Button(onClick = { DestinationsNavigator.navigate(Screen2Destination) }) {
                Text("Screen 2")
            }
            Button(onClick = { DestinationsNavigator.navigate(Screen3Destination) }) {
                Text("Screen 3")
            }
        }
    }
}

@Destination(start = true)
@Composable
fun Screen1() {
    Text("Screen 1")
}

@Destination
@Composable
fun Screen2() {
    Text("Screen 2")
}

@Destination
@Composable
fun Screen3() {
    Text("Screen 3")
}

From what I understand from your Wiki and by looking at your sample, I can only get a DestinationsNavigator impl instance as a param to my screens (via generated code). But in this simple case I need it outside. rememberDestinationsNavController seems to not help me either, since its navigate(...) does not accept my ScreenXDestinations.

Is this not a valid use case? What did I misunderstand?

raamcosta commented 3 years ago

Hi again @StephanSchuster!

In these cases, all navigation APIs from the official compose navigation applies.

You can do: navController.navigate(Screen1.route) for example. If Screen1 does not have navigation arguments. There is also a navigateTo extension function of NavController that accepts a Routed instance. So that would be: navController.navigateTo(Screen1). This is just a convenience method, it does the same as using the jetpack component API one.

To get the navController you can use the rememberDestinationsNavController function. You then need to pass that navController to the DestinationsNavHost function.

StephanSchuster commented 3 years ago

Thank you @raamcosta for your immediate response.

I guess it's too late. I was already watching the sources of the extension method you also mentioned and then missed the "...To()" and got confused. My mistake. All clear now. Maybe at some point a very simple but working example in the readme would help newbies like me. Your official sample has lots of non-navigation related code in it and the docs only mention code fragments.

More important: Now my app crashes.

java.lang.ClassCastException: java.util.LinkedHashSet cannot be cast to java.util.List
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt.AnimatedNavHost$lambda-3(AnimatedNavHost.kt:388)
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt.AnimatedNavHost(AnimatedNavHost.kt:165)
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt.AnimatedNavHost(AnimatedNavHost.kt:91)
        at com.ramcosta.composedestinations.DestinationsNavHostKt.DestinationsNavHost(DestinationsNavHost.kt:49)
        at com.example.nav.ScreensKt.Scaffold(Screens.kt:30)

My versions:

composeVersion = '1.0.4'
accompanistVersion = '0.20.0'
composeNavigationVersion = '2.4.0-beta01'

That brings me to my last questions (for today):

StephanSchuster commented 3 years ago

Okay, it seems to work after updating to the latest versions:

composeVersion = '1.0.5'
accompanistVersion = '0.20.2'
composeNavigationVersion = '2.4.0-beta02'

If possible, I would still appreciate a comment to my last question. Thanks.

raamcosta commented 3 years ago

Yeah, the "To()" suffix is just there so that imports on the IDE don't get all wonky. I will add a sub section on the navigation section explaining navigation above the NavHost level.

Yes, that crash was not directly an issue with this library. If you were using those same versions and doing the navigation code manually, that crash would still happen.

Regarding the future and the versions, I don't expect there to be many restrictions. For now, since the APIs we rely on are not final, and there are a lot of changes happening in their implementation, I include a "tested versions" block in each release. But since this library works as a wrapper to other APIs, once those get stable that won't be needed anymore.

StephanSchuster commented 3 years ago

Ah, alright. Understood. Thanks for clarification.

I now have the the following code with above versions:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun Scaffold() {
    Row(modifier = Modifier.fillMaxSize()) {
        val navController = rememberDestinationsNavController()

        DestinationsMenu(navController = navController)

        DestinationsNavHost(
            modifier = Modifier.fillMaxSize(),
            navController = navController
        )
    }
}

@Composable
fun DestinationsMenu(navController: NavController) {
    val backStackEntry by navController.currentBackStackEntryAsState()
    val destination = backStackEntry?.navDestination ?: NavGraphs.root.startDestination

    NavigationRail {
        NavGraphs.root.destinations.forEach {
            NavigationRailItem(
                icon = { Icon(Icons.Filled.Star, contentDescription = it.key) },
                label = { Text(it.key) },
                selected = it.value == destination,
                onClick = { navController.navigate(it.value.route) }
            )
        }
    }
}

The app starts and then immediately crashes:

java.lang.NoSuchMethodError: No static method AnimatedContent(Landroidx/compose/animation/core/Transition;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V in class Landroidx/compose/animation/AnimatedContentKt; or its super classes (declaration of 'androidx.compose.animation.AnimatedContentKt' appears in /data/app/~~5AEd6a9JA-Yz6TYBzgaeXA==/com.elektrobit.mad.template-4U9S7JjqsHbSKJj0LUx2tA==/base.apk)
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt.AnimatedNavHost(AnimatedNavHost.kt:242)
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt$AnimatedNavHost$10.invoke(Unknown Source:23)
        at com.google.accompanist.navigation.animation.AnimatedNavHostKt$AnimatedNavHost$10.invoke(Unknown Source:10)
        at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
        at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
        at androidx.compose.runtime.ComposerImpl.skipToGroupEnd(Composer.kt:2427)
        at androidx.compose.material.MaterialTheme_androidKt.PlatformMaterialTheme(MaterialTheme.android.kt:24)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:82)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:81)
        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.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
        at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
        at androidx.compose.runtime.ComposerImpl.skipToGroupEnd(Composer.kt:2427)
        at androidx.compose.material.TextKt.ProvideTextStyle(Text.kt:266)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:81)
        at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:80)
        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.material.MaterialThemeKt.MaterialTheme(MaterialTheme.kt:72)
        at com.example.navpoc.template.common.theme.ThemeKt$TemplateTheme$1.invoke(Theme.kt:32)
        at com.example.navpoc.template.common.theme.ThemeKt$TemplateTheme$1.invoke(Theme.kt:31)
        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 com.example.navpoc.template.common.theme.ThemeKt.TemplateTheme(Theme.kt:29)
        at com.example.navpoc.template.common.theme.ThemeKt$TemplateTheme$2.invoke(Unknown Source:10)
        at com.example.navpoc.template.common.theme.ThemeKt$TemplateTheme$2.invoke(Unknown Source:10)
        at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
        at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2158)
        at androidx.compose.runtime.ComposerImpl.skipToGroupEnd(Composer.kt:2427)
        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:107)
        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
        at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)

This most probably has nothing to do with this library but with Accompanist navigation. Still, I wanted to ask if you have any idea.

I then removed the accompanist-navigation-animation dependency from my gradle file. To my understanding the code you generate is then different (right?) and I was hoping to get rid of above issue. But on build I then get the following error:

e: ...\app\build\generated\ksp\debug\kotlin\com\ramcosta\composedestinations\DestinationsNavHost.kt: (38, 77): Unresolved reference: NavBackStackEntry
e: ...\app\build\generated\ksp\debug\kotlin\com\ramcosta\composedestinations\DestinationsNavHost.kt: (69, 77): Unresolved reference: NavBackStackEntry
e: ...\app\build\generated\ksp\debug\kotlin\com\ramcosta\composedestinations\DestinationsNavHost.kt: (99, 77): Unresolved reference: NavBackStackEntry
e: ...\app\build\generated\ksp\debug\kotlin\com\ramcosta\composedestinations\DestinationsNavHost.kt: (119, 77): Unresolved reference: NavBackStackEntry

Looking at the generated code, I don't see an error:

package com.ramcosta.composedestinations

import androidx.compose.animation.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.*
import androidx.navigation.Navigator
import com.ramcosta.composedestinations.spec.DestinationSpec
import com.ramcosta.composedestinations.spec.DestinationStyle
import com.ramcosta.composedestinations.spec.NavGraphSpec
import com.ramcosta.composedestinations.navigation.DependenciesContainerBuilder
import com.ramcosta.composedestinations.navigation.dependency

//region NavHost
/**
 * Like [androidx.navigation.compose.NavHost] but includes the destinations of [navGraph].
 * Composables annotated with `@Destination` will belong to a [NavGraph] inside [NavGraphs].
 *
 * @see [androidx.navigation.compose.NavHost]
 *
 * @param modifier [Modifier]
 * @param startDestination the start destination to use
 * @param navController [NavHostController]
 * @param dependenciesContainerBuilder lambda invoked when a destination gets navigated to. It allows
 * the caller to contribute certain dependencies that the destination can use.
 */
@Composable
fun DestinationsNavHost(
    modifier: Modifier = Modifier,
    startDestination: Destination = NavGraphs.root.startDestination,
    navController: NavHostController = rememberDestinationsNavController(),
    dependenciesContainerBuilder: @Composable DependenciesContainerBuilder.(NavBackStackEntry) -> Unit = {}
) {
    NavHost(
        navController = navController,
        startDestination = startDestination.route,
        modifier = modifier,
        route = NavGraphs.root.route,
    ) {
        addNavGraphDestinations(
            navGraphSpec = NavGraphs.root,
            addNavigation = addNavigation(),   
            addComposable = addComposable(navController, dependenciesContainerBuilder)
        )
    }
}
//endregion NavHost

//region NavController
/**
 * Wraps the correct `remember*NavController` method depending on
 * whether animations are available or not.
 */
@Composable
fun rememberDestinationsNavController(
    vararg navigators: Navigator<out NavDestination>
) = rememberNavController(*navigators)
//endregion

//region internals
private fun addComposable(
    navController: NavHostController,
    dependenciesContainerBuilder: @Composable DependenciesContainerBuilder.(NavBackStackEntry) -> Unit
): NavGraphBuilder.(DestinationSpec) -> Unit {
    return { destination ->
        destination as Destination
        when (val destinationStyle = destination.style) {
            is DestinationStyle.Default -> {
                addComposable(
                    destination,
                    navController,
                    dependenciesContainerBuilder
                )
            }

            is DestinationStyle.Dialog -> {
                addDialogComposable(
                    destinationStyle,
                    destination,
                    navController,
                    dependenciesContainerBuilder
                )
            }

            else -> throw RuntimeException("Should be impossible! Code gen should have failed if using a style for which you don't have the dependency")
        }
    }
}

private fun NavGraphBuilder.addComposable(
    destination: Destination,
    navController: NavHostController,
    dependenciesContainerBuilder: @Composable DependenciesContainerBuilder.(NavBackStackEntry) -> Unit
) {
    composable(
        route = destination.route,
        arguments = destination.arguments,
        deepLinks = destination.deepLinks
    ) { navBackStackEntry ->
        destination.Content(
            navController,
            navBackStackEntry
        ) {
            dependenciesContainerBuilder(navBackStackEntry)
        }
    }
}

private fun NavGraphBuilder.addDialogComposable(
    dialogStyle: DestinationStyle.Dialog,
    destination: Destination,
    navController: NavHostController,
    dependenciesContainerBuilder: @Composable DependenciesContainerBuilder.(NavBackStackEntry) -> Unit
) {
    dialog(
        destination.route,
        destination.arguments,
        destination.deepLinks,
        dialogStyle.properties
    ) { navBackStackEntry ->
        destination.Content(
            navController,
            navBackStackEntry
        ) { dependenciesContainerBuilder(navBackStackEntry) }
    }
}

private fun addNavigation(): NavGraphBuilder.(NavGraphSpec, NavGraphBuilder.() -> Unit) -> Unit {
    return { navGraph, builder ->
        navigation(
            navGraph.startDestination.route,
            navGraph.route
        ) {
            this.builder()
        }
    }
}

//endregion

Any idea?

raamcosta commented 3 years ago

Daaamn, yes I know what was the problem πŸ™

My bad with the latest version, I introduced the import for that in the accompanist specific imports 🀦 I definitely need to streamline my testing with and without accompanist so that this doesn't happen again. I will release a hotfix for this right now, it should be up in maybe 30min or so.

Thanks again for everything. And I'm sorry about this. I will definitely take measures so that it doesn't happen again.

I will comment here once the new version is live on maven central.

StephanSchuster commented 3 years ago

Oh wow, that sounds great.

The latest possible versions currently seem to be:

composeVersion = '1.1.0-beta02'                       <-- latest
composeNavigationVersion = '2.4.0-beta02'             <-- latest
accompanistVersion = '0.21.0-beta'                    <-- not latest

If you release another version anyways, would it be possible to make the latest accompanist 0.21.2-beta work? Currently your generated code does not compile with this.

raamcosta commented 3 years ago

I was working on a new release to support all the new versions but I really want to release this hotfix first.

Later today (or maybe tomorrow) I will make another release to support the new accompanist version.

raamcosta commented 3 years ago

Done. Artifact with version 0.9.2-beta is available on maven central πŸŽ‰

Once again, thank you so much for finding this :) Please check if that is solved for you when you remove accompanist.

As for the other crash you were having, it indeed seems like an issue with that library. Try the versions I'm using in the sample app:

    const val compose = "1.1.0-beta01"
    const val composeNavigation = "2.4.0-beta01"
    const val accompanist = "0.21.0-beta"

And let me know too if that works.

StephanSchuster commented 3 years ago

Thanks for your support.

With accompanist 0.21.0-beta the beta-01 and also the beta-02 of both compose libraries work. I tested them all. As said before, it did work even without your hotfix.

With accompanist 0.21.2-beta (latest version and said to be compatible with compose 1.1.0-beta02) I get compile issues with your library. Would be nice to get them fixed soon.

Regarding the Unresolved reference: NavBackStackEntry: It occurs with:

composeVersion = '1.1.0-beta02'
composeNavigationVersion = '2.4.0-beta02'
accompanistVersion = '0.20.2'
composeDestinationsVersion = '0.9.1-beta'

It does not occur anymore with your HOTFIX:

composeVersion = '1.1.0-beta02'
composeNavigationVersion = '2.4.0-beta02'
accompanistVersion = '0.20.2'
composeDestinationsVersion = '0.9.2-beta'
raamcosta commented 3 years ago

Nice πŸ™‚

Later today or tomorrow morning I'll be releasing the new version to work with latest accompanist.

It envolves some API changes (forced by changes on accompanist) and that's why I want some more time to test and document them.

raamcosta commented 3 years ago

An thank you so much again for this πŸ™

raamcosta commented 3 years ago

0.9.4 is now up with support for the new accompanist version πŸ™‚

Let me know how it goes!

raamcosta commented 2 years ago

Closing this as it should be solved. Thank you so much πŸ™‚