Search code examples
androidandroid-architecture-componentsandroid-jetpackandroid-jetpack-navigation

How can I create dynamic/conditional navigation with Jetpack Navigation?


I've come across an interesting problem with trying to accomplish dynamic or conditional navigation with the Jetpack Navigation library.

The goal I have in mind is to be able to continue using the nav_graph.xml to manage the overall navigation graph, but simultaneously allow for conditional navigation based on some factors.

I have included some code below that shows where my solution is headed. The problem is that it inherently requires a lot of maintenance for future conditional logic to work.

I really want the navigateToDashboard function in the example to be able to be executed with either no parameters, or parameters that rarely change. For instance, instead of passing NavDirections, maybe passing some identifier that let's the navigateToDashboard function know which NavDirections to return.

Code for the class managing the conditional logic.

class DynamicNavImpl(private val featureFlagService: FeatureFlagService) : DynamicNav {

    override fun navigateToDashboard(navDirectionsMap: Map<Int, NavDirections>): NavDirections {
        val destinationIdRes = if (featureFlagService.isDashboardV2Enabled()) {
            R.id.dashboardV2Fragment
        } else {
            R.id.dashboardFragment
        }

        return navDirectionsMap[destinationIdRes] ?: handleNavDirectionsException(destinationIdRes)
    }

    private fun handleNavDirectionsException(destinationIdRes: Int): Nothing {
        throw IllegalStateException("Destination $destinationIdRes does not have an accompanying set of NavDirections. Are you sure you added NavDirections for it?")
    }
}

Call site examples

navigate(
        dynamicNav.navigateToDashboard(
                mapOf(
                        Pair(R.id.dashboardFragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboard()),
                        Pair(R.id.dashboardV2Fragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboardV2())
                )
        )
)

navigate(
        dynamicNav.navigateToDashboard(
                mapOf(
                        Pair(R.id.dashboardFragment, EmailLoginFragmentDirections.emailLoginToDashboard()),
                        Pair(R.id.dashboardV2Fragment, EmailLoginFragmentDirections.emailLoginToDashboardV2())
                )
        )
)

Looking at the call site, you could see how this could be problematic. If I ever want to add a new potential destination, let's say dashboardV3Fragment, then I'd have to go to each call site and add another Pair.

This almost defeats the purpose of having the DynamicNavImpl class. So this is where I am stuck. I want to be able to encapsulate the various variables involved in deciding what destination to go to, but it seems with how NavDirections are implemented, I'm not able to.


Solution

  • I went between a few different approaches, and I landed on something that still doesn't feel ideal, but works for my use case.

    I completely abandoned the idea of using a central dynamic navigation manager. Instead, I decided on having a "redirect" or "container" Fragment that decides what Fragment to show.

    So here's the new code inside of the DashboardRedirectFragment

    childFragmentManager.beginTransaction().replace(
                    R.id.dashboard_placeholder,
                    if (featureFlagService.isDashboardV2Enabled()) {
                        DashboardV2Fragment.newInstance()
                    } else {
                        DashboardFragment.newInstance()
                    }
            ).commit()
    

    The way I'm using this is by registering a new destination in my nav graph called dashboardRedirectFragment, and anything in the graph that needs access to the dashboard use the dashboardRedirectFragment destination.

    This fully encapsulates the dynamic navigation logic in the redirect Fragment, and allows me to continue using my nav graph as expected.