Navigating to Jetpack Compose by Google

In this article, I will describe google’s approach on how to organize navigation in an android project on a pure compose UI.

Adding Gradle Dependencies

Open the file app/build.gradle.kts and add the dependencies to the dependencies section navigation-compose

dependencies {
    implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
}

Installing NavController

NavController is the root of the compose navigation, which is responsible for the backstack of composable functions, forward navigation, backward state management.

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App()
        }
    }
}
@Composable
fun App() {
    AppTheme {
        val appState = rememberAppState()
        Scaffold(
                    bottomBar = {
                        if (appState.shouldShowBottomBar) {
                            BottomBar(
                                    tabs = appState.bottomBarTabs,
                                    currentRoute = appState.currentRoute!!,
                                    navigateToRoute = appState::navigateToBottomBarRoute
                            )
                        }
                    },
                    scaffoldState = appState.scaffoldState
            ) {
            NavHost(
                    navController = appState.navController,
                    startDestination = MainDestinations.HOME_ROUTE
            ) {
                navGraph()
            }
        }
    }
}

Add a subscriber to track navigation state change events rememberAppState ()

@Composable
fun rememberAppState(
        scaffoldState: ScaffoldState = rememberScaffoldState(),
        navController: NavHostController = rememberNavController()
) =
        remember(scaffoldState, navController) {
            AppState(scaffoldState, navController)
        }      

Define the Navigation Graph, indicate the name of the route graph and the startDestination start screen (the type of set values ​​String)

fun NavGraphBuilder.navGraph() {
    navigation(
            route = MainDestinations.HOME_ROUTE,
            startDestination = HomeSections.CATALOG.route
    ) {
        addHomeGraph()
    }
}

Navigation graph

Determine what screens will be in the graph

fun NavGraphBuilder.addHomeGraph(
        modifier: Modifier = Modifier
) {
    composable(HomeSections.CATALOG.route) {
        CatalogScreen()
    }
    composable(HomeSections.PROFILE.route) {
        ProfileScreen()
    }
    composable(HomeSections.SEARCH.route) {
        SearchScreen()
    }
}

Let’s add the MainDestinations object, which will contain the names of the screens that will be navigated through.

object MainDestinations {
    const val HOME_ROUTE = "home"
    const val GAME_CARD_DETAIL_ROUTE = "cardRoute"
    const val GAME_CARD = "gameCard"
    const val SUB_CATALOG_ROUTE = "subCatalog"
    const val CATALOG_GAME = "catalogGame"
}

Add an enum class containing a list of tabs to bottomNavigation

enum class HomeSections(
        @StringRes val title: Int,
        val icon: ImageVector,
        val route: String
) {
    CATALOG(R.string.home_catalog, Icons.Outlined.Home, "$HOME_ROUTE/catalog"),
    PROFILE(R.string.home_profile, Icons.Outlined.AccountCircle, "$HOME_ROUTE/profile"),
    SEARCH(R.string.home_search, Icons.Outlined.Search, "$HOME_ROUTE/search")
}

Add a class working with the navigation state AppState.kt

@Stable
class AppState(
        val scaffoldState: ScaffoldState,
        val navController: NavHostController
) {
    // ----------------------------------------------------------
    // Источник состояния BottomBar
    // ----------------------------------------------------------

    val bottomBarTabs = HomeSections.values()
    private val bottomBarRoutes = bottomBarTabs.map { it.route }

    // Атрибут отображения навигационного меню bottomBar
    val shouldShowBottomBar: Boolean
        @Composable get() = navController
                .currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes

    // ----------------------------------------------------------
    // Источник состояния навигации
    // ----------------------------------------------------------

    val currentRoute: String?
        get() = navController.currentDestination?.route

    fun upPress() {
        navController.navigateUp()
    }
		
    // Клик по навигационному меню, вкладке.
    fun navigateToBottomBarRoute(route: String) {
        if (route != currentRoute) {
            navController.navigate(route) {
                launchSingleTop = true
                restoreState = true
                //Возвращаем выбранный экран, 
                //иначе если backstack не пустой то показываем ранее открытое состяние
                popUpTo(findStartDestination(navController.graph).id) {
                    saveState = true
                }
            }
        }
    }
}

private fun NavBackStackEntry.lifecycleIsResumed() =
        this.lifecycle.currentState == Lifecycle.State.RESUMED

private val NavGraph.startDestination: NavDestination?
    get() = findNode(startDestinationId)

private tailrec fun findStartDestination(graph: NavDestination): NavDestination {
    return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph
}

The intermediate result is described above. Added graph, navigation menu, described screens that will participate in navigation. Next, I will describe two ways how to pass parameters from one screen to another.

Passing Arguments

Example 1. Serializing the passed object to String json

Let’s add a function to AppState.kt to go to the card with the game. The passed object in my case was the data class GameCard should be marked with the Serializable annotation.

fun navigateToGameCard(game: GameCard, from: NavBackStackEntry) {
        //Проверяем ЖЦ навигации, чтобы избавиться от повторяющихся событий. Ложных нажатий.
        if (from.lifecycleIsResumed()) {
            navigateModel(
                    route = MainDestinations.GAME_CARD_DETAIL_ROUTE,
                    model = game
            )
        }
}
    
inline fun <reified T> navigateModel(route: String, model: T) {
    val json = Json.encodeToString(model)
    navController.navigate("$route/$json")
}

Note that the navController.navigate (..) method takes a String type as input i.e. route containing the path to open and the argument after /

Now we will decrypt the data on the receiving side and go to the card with the game

Modify addHomeGraph by adding a composable card opener.

fun NavGraphBuilder.addHomeGraph(
        upPress: () -> Unit
) {
    composable(
            route = "${MainDestinations.GAME_CARD_DETAIL_ROUTE}/{${MainDestinations.GAME_CARD}}",
            arguments = listOf(navArgument(MainDestinations.GAME_CARD) { type = NavType.StringType })
    ) { backStackEntry ->
        val arguments = requireNotNull(backStackEntry.arguments)
        arguments.getString(MainDestinations.GAME_CARD)?.let { cardDataString ->
            val card = Json.decodeFromString<GameCard>(cardDataString)
            CardDialog(card, upPress)
        }
    }
}

We indicate route, which takes as input the path name MainDestinations.GAME_CARD_DETAIL_ROUTE and the MainDestinations.GAME_CARD string object that we open. Next parameter arguments which contains a list of arguments of primitive types.

Example 2. Passing a parameter

fun navigateToGameCard(game: Int, from: NavBackStackEntry) {
        //Проверяем ЖЦ навигации, чтобы избавиться от повторяющихся событий. Ложных нажатий.
        if (from.lifecycleIsResumed()) {
            navController.navigate("${MainDestinations.GAME_CARD_DETAIL_ROUTE}/$game")
        }
    }
composable(
            route = "${MainDestinations.GAME_CARD_DETAIL_ROUTE}/{${MainDestinations.GAME_CARD}}",
            arguments = listOf(navArgument(MainDestinations.GAME_CARD) { type = NavType.IntType })
    ) { backStackEntry ->
        val arguments = requireNotNull(backStackEntry.arguments)
        val gameCardId = arguments.getInt(MainDestinations.GAME_CARD, 0)
        if(gameCardId != 0)
            CardDialog(gameCardId, upPress, {}, {})
    }

A distinctive feature is the transfer of the ID card with its subsequent request to the database to retrieve all the necessary data.

Note: the only disappointment was that now, when navigating, it is necessary to set the transition route route in a string format, whereas for ordinary jetpack navigation, id for fragments were set in the generated graph and the system created a list of routes in resources. In order to create less logical errors, I recommend putting the route names in a separate file that will be responsible for this.

The repository with the considered navigation can be found at github

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *