What is the correct way to implement material app with toolbar, bottom bar and drawer in compose?

1k views Asked by At

Before Jetpack Compose I was using Navigation Component in the projects in View system world.

Apps had only one activity - toolbar, bottom bar and drawer were added only to this activity once.

Apps could have many screens (fragments) and only top destination fragments were displaying bottom bar and allowed drawer, for other fragments it was hidden.

All of that was handled with Navigation Component like this from the activity:

fun initNavigation() {
    val topLevelDestinationFragments = setOf(R.id.homeFragment, R.id.photosFragment)
    
    appBarConfiguration = AppBarConfiguration(
        topLevelDestinationFragments,
        binding.drawerLayout
    )
    setupActionBarWithNavController(navController, appBarConfiguration)
    binding.drawerNavigationView.setupWithNavController(navController)
    binding.bottomNavigationView.setupWithNavController(navController)
    
    navController.addOnDestinationChangedListener { _, destination, _ ->
        // don't change bars if a dialog fragment
        if (destination is FloatingWindow) {
            return@addOnDestinationChangedListener
        }
        
        // Google solution to hide navigation bars
        // https://developer.android.com/guide/navigation/navigation-ui#listen_for_navigation_events
        
        val allowBottomAndDrawerNavigation = destination.id in topLevelDestinationFragments
        
        binding.bottomNavigationView.isVisible = allowBottomAndDrawerNavigation
        
        binding.drawerLayout.setDrawerLockMode(
            if (allowBottomAndDrawerNavigation) {
                DrawerLayout.LOCK_MODE_UNLOCKED
            } else {
                DrawerLayout.LOCK_MODE_LOCKED_CLOSED
            }
        )
        
        binding.toolbar.isVisible = // if we need to hide toolbar for specific fragments
    }
}


override fun onSupportNavigateUp(): Boolean {
    return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}

Quite easy, no need to add separately toolbar for each fragment and so on (so it was added only to the layout of the activity, it wasn't added to each layout of each fragment)

Navigation Component automatically handles back and menu (for drawer) buttons on the toolbar, automatically switches between them because it knows top destinations

Is there something similar for Compose?

Because I checked official Google sample "JetNews" from CodeLabs git https://github.com/googlecodelabs/android-compose-codelabs/tree/end/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui

They use compose navigation there but they separately added Scaffold for each compose screen.

For example "Home" compose screen has it own Scaffold

Scaffold(
    scaffoldState = scaffoldState,
    topBar = {
        val title = stringResource(id = R.string.app_name)
        InsetAwareTopAppBar(
            title = { Text(text = title) },
            navigationIcon = {
                IconButton(onClick = { coroutineScope.launch { openDrawer() } }) {
                    Icon(
                        painter = painterResource(R.drawable.ic_jetnews_logo),
                        contentDescription = stringResource(R.string.cd_open_navigation_drawer)
                    )
                }
            }
        )
    }
)

And "Article" compose screen has it own Scaffold

Scaffold(
    topBar = {
        InsetAwareTopAppBar(
            title = {
                Text(
                    text = "Published in: ${post.publication?.name}",
                    style = MaterialTheme.typography.subtitle2,
                    color = LocalContentColor.current
                )
            },
            navigationIcon = {
                IconButton(onClick = onBack) {
                    Icon(
                        imageVector = Icons.Filled.ArrowBack,
                        // Step 4: Content descriptions
                        contentDescription = stringResource(
                            R.string.cd_navigate_up
                        )
                    )
                }
            }
        )
    }
)

So basically here we duplicate the code and define manually different logic for navigationIcon (icon and action) of toolbar

Does it mean that if we have some details screens with back arrow button and without bottom bar then we define a separate Scaffold and can't just use one Scaffold for all compose screens of the app?

Or can we implement the same logic as well for all compose screens as we did with Navigation Component in View system? Also setting top destinations, hiding bottom bar and locking drawer for non top destinations.

1

There are 1 answers

0
user924 On

I guess the correct way is to set top level Scaffold (where we define theme and navigation) with AppDrawer and BottomBar and add some logic if it should be added to a screen by checking the current navigation route:

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

e.g.:

drawerContent = if (isTopLevelDestination) {
    {
        AppDrawer(
            ...
        )
    }
} else {
    null
},
bottomBar = {
    if (isTopLevelDestination) {
        AppBottomBar(
            ...
        )
    }
}

But with TopAppBar it's not that easy. We can't really add it to top level Scaffold in case when toolbar can have actions for specific screens

That's why each screen additionally defines its Scaffold with TopAppBar configured as needed.

Only if an app is very simple and toolbar doesn't have any actions for all screens of the app, just hardcoded titles for all screens, then we can define TopAppBar in top level Scaffold but for more complicated apps each screen should define its own toolbar