compose-destinations: Cannot use BottomSheets navigation without AnimatedNavigator

Example how I’d expect it work:

@Composable
fun DefaultNavHost() {
    val bottomSheetNavigator = rememberBottomSheetNavigator()
    val navController = rememberNavController(bottomSheetNavigator)
    ModalBottomSheetLayout(
        bottomSheetNavigator = bottomSheetNavigator,
        sheetShape = defaultSheetShape,
        sheetBackgroundColor = BgDefault
    ) {
        DestinationsNavHost(
            navController = navController,
            navGraph = NavGraphs.root,
            dependenciesContainerBuilder = {
                val onboardingViewModel: OnboardingViewModel = get()
                dependency(onboardingViewModel)
            }
        )
    }
}

I would like to avoid using animations-lib in my project because they change default animation that I need. The code above gives error:

FATAL EXCEPTION: main
java.lang.IllegalStateException: You need to use 'rememberAnimatedNavHostEngine' to get an engine that can use BottomSheet and pass that into the 'DestinationsNavHost'

OK, I add this line engine = rememberAnimatedNavHostEngine(), Now error is:

FATAL EXCEPTION: main
java.lang.IllegalStateException: Could not find Navigator with name "animatedComposable". You must call NavController.addNavigator() for each navigation type.

so I assume it’s impossible to use BottomSheets without animation. However, that is possible with pure Compose navigation. Can this be fixed? Or what I’m doing wrong

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 18 (8 by maintainers)

Most upvoted comments

I got same error:

FATAL EXCEPTION: main
java.lang.IllegalStateException: Could not find Navigator with name "animatedComposable". You must call NavController.addNavigator() for each navigation type.

It turns out we must use rememberAnimatedNavController() and not rememberNavController()

When using:

rememberAnimatedNavHostEngine()

👍🏻

The reason is to disable transactions right? If I have some time today I’ll look into it. It should be similar to what you did, I’ll let you know 🙂

NavGraphs.root.nestedGraphs() or something similar to get nested graphs 😁

Creating your own engine is something you definitely shouldn’t need to do though. Disabling animations should be easy. I’ve done it in the past similar to what you did, but I cannot remember from top of my head 🤔

OK, I managed to make it work by:

        val scope = remember {
            object : DestinationScopeImpl<T>(
                destination = destination,
                navBackStackEntry = navBackStackEntry,
                navController = navController
            ) {}
        }

However, I had to use the animations-core dependency, otherwise I got exception:

    ext.composeDestinationsVersion = '1.6.20-beta'
    implementation "io.github.raamcosta.compose-destinations:animations-core:$composeDestinationsVersion"
    ksp "io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion"

So, after I failed above I tried another way - creating my own DefaultNavHostEngine.

First of all, I changed compose-destinations dependency to core and added dependency to navigation-material:

    ext.composeDestinationsVersion = '1.6.20-beta'
    implementation "io.github.raamcosta.compose-destinations:core:$composeDestinationsVersion"
    ksp "io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion"
    implementation "com.google.accompanist:accompanist-navigation-material:0.26.5-rc"

Then copied impl from the lib and adjusted:


/**
 * Returns the default [NavHostEngine] to be used with normal (non-animated)
 * core ("io.github.raamcosta.compose-destinations:core").
 *
 * The [NavHostEngine] is used by default in [com.ramcosta.composedestinations.DestinationsNavHost]
 * call.
 */
@Composable
fun rememberDefaultNavHostEngine(): NavHostEngine = remember {
    DefaultNavHostEngine()
}

internal class DefaultNavHostEngine : NavHostEngine {

    override val type = NavHostEngine.Type.DEFAULT

    @Composable
    override fun rememberNavController(
        vararg navigators: Navigator<out NavDestination>
    ) = androidx.navigation.compose.rememberNavController(*navigators)

    @Composable
    override fun NavHost(
        modifier: Modifier,
        route: String,
        startRoute: Route,
        navController: NavHostController,
        builder: NavGraphBuilder.() -> Unit
    ) {
        androidx.navigation.compose.NavHost(
            navController = navController,
            startDestination = startRoute.route,
            modifier = modifier,
            route = route,
            builder = builder
        )
    }

    override fun NavGraphBuilder.navigation(
        navGraph: NavGraphSpec,
        builder: NavGraphBuilder.() -> Unit
    ) {
        navigation(
            startDestination = navGraph.startRoute.route,
            route = navGraph.route,
            builder = builder
        )
    }

    override fun <T> NavGraphBuilder.composable(
        destination: DestinationSpec<T>,
        navController: NavHostController,
        dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
        manualComposableCalls: ManualComposableCalls,
    ) {
        when (val destinationStyle = destination.style) {
            is DestinationStyle.Runtime,
            is DestinationStyle.Default -> {
                addComposable(
                    destination,
                    navController,
                    dependenciesContainerBuilder,
                    manualComposableCalls
                )
            }

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

            is DestinationStyle.BottomSheet -> {
                addBottomSheetComposable(
                    destination,
                    navController,
                    dependenciesContainerBuilder,
                    manualComposableCalls
                )
            }

            else -> throw IllegalStateException("You need to use 'rememberAnimatedNavHostEngine' to get an engine that can use ${destinationStyle.javaClass.simpleName} and pass that into the 'DestinationsNavHost' ")
        }
    }

    private fun <T> NavGraphBuilder.addComposable(
        destination: DestinationSpec<T>,
        navController: NavHostController,
        dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
        manualComposableCalls: ManualComposableCalls,
    ) {
        @SuppressLint("RestrictedApi")
        val contentLambda = manualComposableCalls[destination.baseRoute]

        composable(
            route = destination.route,
            arguments = destination.arguments,
            deepLinks = destination.deepLinks
        ) { navBackStackEntry ->
            CallComposable(
                destination,
                navController,
                navBackStackEntry,
                dependenciesContainerBuilder,
                contentLambda
            )
        }
    }

    private fun <T> NavGraphBuilder.addDialogComposable(
        dialogStyle: DestinationStyle.Dialog,
        destination: DestinationSpec<T>,
        navController: NavHostController,
        dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
        manualComposableCalls: ManualComposableCalls
    ) {
        @SuppressLint("RestrictedApi")
        val contentLambda = manualComposableCalls[destination.baseRoute]

        dialog(
            destination.route,
            destination.arguments,
            destination.deepLinks,
            dialogStyle.properties
        ) { navBackStackEntry ->
            CallComposable(
                destination,
                navController,
                navBackStackEntry,
                dependenciesContainerBuilder,
                contentLambda
            )
        }
    }

    private fun <T> NavGraphBuilder.addBottomSheetComposable(
        destination: DestinationSpec<T>,
        navController: NavHostController,
        dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
        manualComposableCalls: ManualComposableCalls
    ) {
        @SuppressLint("RestrictedApi")
        val contentWrapper = manualComposableCalls[destination.baseRoute]

        bottomSheet(
            destination.route,
            destination.arguments,
            destination.deepLinks
        ) { navBackStackEntry ->
            CallComposable(
                destination,
                navController,
                navBackStackEntry,
                dependenciesContainerBuilder,
                contentWrapper
            )
        }
    }

    @Suppress("UNCHECKED_CAST")
    @Composable
    private fun <T> CallComposable(
        destination: DestinationSpec<T>,
        navController: NavHostController,
        navBackStackEntry: NavBackStackEntry,
        dependenciesContainerBuilder: @Composable DependenciesContainerBuilder<*>.() -> Unit,
        contentLambda: DestinationLambda<*>?
    ) {
        val scope = remember {
            DestinationScopeImpl.Default(
                destination,
                navBackStackEntry,
                navController
            )
        }

        if (contentLambda == null) {
            with(destination) { scope.Content(dependenciesContainerBuilder) }
        } else {
            contentLambda as DestinationLambda<T>
            contentLambda(scope)
        }
    }
}

I’ve added is DestinationStyle.BottomSheet case to default implementation. I almost succeeded, the thing is that DestinationScopeImpl.Default from CallComposable is internal in the lib:

Screenshot 2022-10-11 at 13 01 57

Is there a way to workaround it?