Jetpack Compose - Bottom navigation bar isn't showing with nested navGraph - android

I'm developing an android application using Jetpack Compose. For now, I have several screens (Login, Register, Messages, Profile and Settings). I used a root NavGraph to host the navGraphs of authentication and main screen (See the codes below).
RootNavGraph.kt
#Composable
fun RootNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = AUTHENTICATION_ROUTE,
route = ROOT_ROUTE
) {
authNavGraph(navController = navController)
bottomNavGraph(navController = navController)
}
}
authNavGraph.kt
fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
navigation(
startDestination = AuthenticationScreens.Login.route,
route = AUTHENTICATION_ROUTE
) {
composable(route = AuthenticationScreens.Login.route) {
LoginPage(navController = navController)
}
composable(route = AuthenticationScreens.Register.route) {
RegisterPage(navController = navController)
}
}
}
bottomNavGraph.kt
fun NavGraphBuilder.bottomNavGraph(navController: NavHostController) {
navigation(
startDestination = BottomBarScreen.Messages.route,
route = BOTTOM_ROOT_ROUTE
) {
composable(route = BottomBarScreen.Messages.route) {
Messages()
}
composable(route = BottomBarScreen.Profile.route) {
Profile(navController = navController)
}
composable(route = BottomBarScreen.Settings.route) {
Settings()
}
}
}
I have the MainScreen that contains the bottom navigation bar.
#Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomBar(navController = navController)
}
) {
//bottomNavGraph(navController)
}
}
#Composable
fun BottomBar(navController: NavHostController) {
val screens = listOf(
BottomBarScreen.Messages,
BottomBarScreen.Profile,
BottomBarScreen.Settings
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation() {
screens.forEach { screen ->
AddItem(
screen = screen,
currentDestination = currentDestination,
navController = navController
)
}
}
}
#Composable
fun RowScope.AddItem(
screen: BottomBarScreen,
currentDestination: NavDestination?,
navController: NavHostController
) {
BottomNavigationItem(
label = {
Text(text = screen.title)
},
alwaysShowLabel = false,
icon = {
Icon(imageVector = screen.icon, contentDescription = "Navigation Icon")
},
selected = currentDestination?.hierarchy?.any {
it.route == screen.route
} == true,
unselectedContentColor = LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id)
launchSingleTop = true
}
}
)
}
My problem is: after switching to use the nested nav graphs, I cannot use the bottomNavGraph directly in the MainScreen since it's no longer a NavHost and thus the bottom navigation bar isn't showing. I would appreciate if any one can help me with that.

For bottom navigation use the NavHost to show the bottom bar
fun HomeNavGraph(navHostController: NavHostController) {
NavHost(
navController = navHostController,
route = Graph.Home,
startDestination = BottomBar.Home.route
) {
composable(BottomBar.Home.route){
HomeScreenContent()
}
then use this graph in your homescreen scaffold
fun HomeScreen(navController: NavHostController = rememberNavController()) {
Scaffold(bottomBar = {
BottomNavigation(navController = navController)
}) {
HomeNavGraph(navHostController = navController)
}
at last in the rootgraph use this homescreen class with your rootgraph route
fun RootNavigationGraphBuilder(navHostController: NavHostController) {
NavHost(navController = navHostController, startDestination = Graph.Authentication, route = Graph.Root) {
authNavGraph(navHostController)
composable(route = Graph.Home){
HomeScreen()
}
}
here are sample routes which i use in my code
object Graph {
const val Root = "root_graph"
const val Authentication = "auth_graph"
const val Home = "home_graph"
}

Related

Setting up a compose scaffold with NavHost at higher level

I am trying to setup an application flow in which there is a main route/screen, followed by a home route/screen where the home screen contains a scaffold to setup bottom bar navigation.
I originally had the scaffold setup at the main (top level) route where the scaffold content was just the NavHost ie:
#Composable
fun MainScreen() {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerGesturesEnabled = false,
drawerContent = {...},
bottomBar = {...}
) {
NavHost(
navController = navController,
startDestination = "tab1"
) {
tab1Graph(navController)
tab2Graph(navController)
tab3Graph(navController)
}
}
}
}
Which is fine I suppose, however since only my home route needs a scaffold, why have the scaffold at the top level instead of at the lower level in which its needed.
Here is my attempt to move the scaffold into the home screen:
fun NavGraphBuilder.homeGraph(
navController: NavController,
bottomSheetNavigator: BottomSheetNavigator
) {
composable("home") {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
ModalBottomSheetLayout(bottomSheetNavigator) {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {...},
bottomBar = {...}
) {
// Not entirely sure how to setup bottom nav tabs within the scaffold?
}
}
}
}
However I am lost at how to get the tab content to live inside the scaffold based on route. EG the same magic that happens when you embed the NavHost inside the scaffold.
I'm currently working on a project where I solved the same problem.
First, in the MainActivity I call my MainNavGraph, then in the main NavGraph, I call my HomeScreen Composable which contains the BottomNavGraph and the screens to display in this HomeScreen. Finally, in the BottomNavGraph I include everything related to the HomeScreen
MainActivity :
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
}
val yourViewModel = viewModel(modelClass = YourViewModel::class.java)
YourProjectTheme {
MainNavGraph(yourViewModel)
}
}
}
Main navigation
#Composable
fun MainNavGraph(
yourMainViewModel: YourMainViewModel?,
){
val navController = rememberNavController()
NavHost(
navController = NavController,
startDestination = "top_level_composable"
){
composable("top_level_composable"){
TopLevelComposable{
navController.navigate("home_screen")
}
}
composable("home_screen"){
home()
}
}
}
Home Screen
#Composable
fun HomeScreen(){
val homeNavController = rememberNavController()
val anotherViewModel = viewModel(modelClass = AnotherViewModel::class.java)
Scaffold(
...
...
bottomBar = { BottomNavigationBar(navController) }
content = { padding ->
Box(modifier = Modifier.padding(padding)){
HomeNavGraph(
navController = homeNavController,
anotherViewModel = anotherViewModel
)
}
}
)
}
HomeNavGraph
#Composable
fun HomeNavGraph(
navController: NavHostController,
anotherViewModel: AnotherViewModel
) {
NavHost(
navController = navController,
route = "home_nav",
startDestination = "welcome"
){
composable("welcome"){
WelcomeScreen(navController)
}
composable("posts"){
PostsScreen(navController, anotherViewModel)
}
composable("search"){
SearchScreen(navController)
}
composable("messages"){
MessagesScreen(navController)
}
composable("profile"){
ProfileScreen(navController)
}
}
}
BottomNavigation
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
"welcome",
"posts",
"search",
"messages",
"profile",
)
BottomNavigation(
backgroundColor = Color.White,
contentColor = Color.Black,
modifier = Modifier.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)),
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
label = { Text(text = item) },
selectedContentColor = GWpalette.ImperialRed,
unselectedContentColor = GWpalette.Gunmetal,
alwaysShowLabel = false,
selected = currentRoute == item,
onClick = {
navController.navigate(item) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
You can see how to create Bottom Navigation Bar with Jetpack Compose here
https://johncodeos.com/how-to-create-bottom-navigation-bar-with-jetpack-compose/
You can conditionally use the Scaffold based on the current route:
val navController = rememberNavController()
val navBackStateEntry by navController.currentBackStackEntryAsState()
if (navBackStateEntry?.destination?.route == "my_route") {
Scaffold(...)
} else {
Text("No scaffold")
}

Jetpack Compose BottomNavigation keep item selected when navigating in

I just started using Jetpack Compose and wanted to try out the BottomNavigation. A basic implementation with three items was no problem. Now, one of the three screens should navigate to a detail screen when clicking on a list item. The problem that occurs, is that on the detail screen the bottom navigation item is not selected anymore.
Here's my implementation:
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WhoHasItTheme {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigationBar(
items = listOf(
BottomNavItem(
name = "Home",
route = Screen.Home.route,
icon = Icons.Default.Home
),
BottomNavItem(
name = "Search",
route = Screen.GameListScreen.route,
icon = Icons.Default.Search
),
BottomNavItem(
name = "Profile",
route = Screen.Profile.route,
icon = Icons.Default.Person
)
),
navController = navController,
onItemClick = {
navController.navigate(it.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
Navigation(navController = navController)
}
}
}
}
}
}
#Composable
fun Navigation(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen()
}
composable(Screen.GameListScreen.route) {
GameListScreen(navController)
}
composable(
route = "${Screen.GameDetailScreen.route}/{gameId}",
arguments = listOf(navArgument("gameId") { type = NavType.IntType })
) {
GameDetailScreen()
}
composable(Screen.Profile.route) {
ProfileScreen()
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomNavigationBar(
items: List<BottomNavItem>,
navController: NavController,
modifier: Modifier = Modifier,
onItemClick: (BottomNavItem) -> Unit
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation(
modifier
) {
items.forEach { item ->
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
onClick = { onItemClick(item) },
selectedContentColor = MaterialTheme.colors.onSurface,
label = { Text(item.name) },
alwaysShowLabel = false,
icon = {
Column(horizontalAlignment = CenterHorizontally) {
if (item.badgeCount > 0) {
BadgeBox(
badgeContent = {
Text(item.badgeCount.toString())
}
) {
Icon(imageVector = item.icon, contentDescription = item.name)
}
} else {
Icon(imageVector = item.icon, contentDescription = item.name)
}
}
}
)
}
}
}
In my understanding currentDestination?.hierarchy should also include the root screen (GameListScreen). What am I understanding wrong here and how can I make it so screens below the root screen of each bottom navigation item still have their tab item as 'selected'?

Navigating to a Bottom Tab Screen from a Single screen with a button Jetpack Compose

I have a auth page and after the auth page, I basically navigate to a Tabbed application.
The problem is that bottom bar disappears once I click on tab.
Below is what my code looks like
sealed class ScreenM(val route: String) {
object Landing: Screen("landingm")
object Tab: Screen("tabm")
}
sealed class Screen(val route: String) {
object PasswordLogin: Screen("passwordlogin")
object TabBar: Screen("tabbar")
}
sealed class TabbarItem(var route: String, var icon: Int, var title: String) {
object Home : TabbarItem("tabhome", R.drawable.ic_home_tab_icon, "Home")
object Profile : TabbarItem("tabprofile", R.drawable.ic_profile_tab_icon, "Profile")
}
my application entry point is
#Composable
fun App() {
val navController = rememberNavController()
NavHost(navController, startDestination = ScreenM.Landing.route) {
addLandingTopLevel(navController = navController)
addTabBarTopLevel(navController = navController)
}
}
private fun NavGraphBuilder.addLandingTopLevel(
navController: NavController,
) {
navigation(
route = ScreenM.Landing.route,
startDestination = Screen.Home.route
) {
addPasswordLogin(navController)
}
}
private fun NavGraphBuilder.addPasswordLogin(navController: NavController) {
composable(route = Screen.PasswordLogin.route) {
PasswordLoginView(navController)
}
}
private fun NavGraphBuilder.addTabBarTopLevel(
navController: NavController,
) {
navigation(
route = ScreenM.Tab.route,
startDestination = Screen.TabBar.route
) {
addTabBar(navController)
addHome(navController)
addProfile(navController)
}
}
private fun NavGraphBuilder.addTabBar(navController: NavController) {
composable(route = Screen.TabBar.route) {
TabBarView(navController)
}
}
private fun NavGraphBuilder.addHome(navController: NavController) {
composable(route = TabbarItem.Home.route) {
HomeView()
}
}
private fun NavGraphBuilder.addProfile(navController: NavController) {
composable(route = TabbarItem.Profile.route) {
ProfileView()
}
}
I am triggering the Tab like this
// ...
NavigationButton(buttonText = "Login", onBackPressed = {
navController.popBackStack()
}) {
navController.navigate(ScreenM.Tab.route)
}
// ...
Then my Tabbar is like
#Composable
fun TabBarView(navController: NavController) {
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
}
}
Then bottom navigation Bar is like this
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
TabbarItem.Home,
TabbarItem.Profile
)
BottomNavigation(
backgroundColor = colorResource(id = R.color.white),
contentColor = Color.Black
) {
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
label = { Text(text = item.title) },
selectedContentColor = Color.Red,
unselectedContentColor = Color.Blue.copy(0.4f),
alwaysShowLabel = true,
selected = false,
onClick = {
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
You have two options.
Displaying BottomNavigationBar inside each tab
1.1. Not sure what's addTabBar navigation route in your code, I don't think it's need, as looks like you only have two tabs: TabbarItem.Home and TabbarItem.Profile.
1.2. You can add BottomNavigationBar inside each view, specifying the selected item. Inside HomeView it can look like this:
BottomNavigationBar(navController, selectedTab = TabbarItem.Home)
1.3. Depending on the selected tab, you need to select the needed item of BottomNavigationBar
#Composable
fun BottomNavigationBar(navController: NavController, selectedTab: TabbarItem) {
val items = listOf(
TabbarItem.Home,
TabbarItem.Profile
)
BottomNavigation(
// ...
) {
items.forEach { item ->
BottomNavigationItem(
// ...
selected = selectedTab == item,
// ...
Having a single navigation bar outside of NavHost, you can find an example in documentation

hide Top and Bottom Navigator on a specific screen inside Scaffold Jetpack Compose

I'm creating a simple app with bottom navigation and drawer.
I wrap all screens inside a Scaffold with topbar and bottom bar.
I want to hide top bar and bottom bar on a specific screen. Does anyone know to how achieve that
here is the code for setting up navigation.
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
Scaffold(
bottomBar = {
AppBottomBar(navController)
},
topBar = {
AppTopBar(scaffoldState)
},
drawerContent = {
DrawerContent(navController, scaffoldState)
},
scaffoldState = scaffoldState
) {
// ovoid bottom bar overlay content
Column(modifier = Modifier.padding(bottom = 58.dp)) {
AppNavigation(navController)
}
}
AppNavigation contains NavHost for navigating to screens
I recommend you use AnimatedVisibility for BottomNavigation widget and TopAppBar widget, im my opinion it's clearest way for compose.
You should use remeberSaveable to store state of BottomBar and TopAppBar:
// State of bottomBar, set state to false, if current page route is "car_details"
val bottomBarState = rememberSaveable { (mutableStateOf(true)) }
// State of topBar, set state to false, if current page route is "car_details"
val topBarState = rememberSaveable { (mutableStateOf(true)) }
In composable function we used when for control state of BottomBar and TopAppBar, below we set bottomBarState and topBarState to true, if we would like to show BottomBar and TopAppBar, otherwise we set bottomBarState and topBarState to false:
val navController = rememberNavController()
// Subscribe to navBackStackEntry, required to get current route
val navBackStackEntry by navController.currentBackStackEntryAsState()
// Control TopBar and BottomBar
when (navBackStackEntry?.destination?.route) {
"cars" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"bikes" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"settings" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"car_details" -> {
// Hide BottomBar and TopBar
bottomBarState.value = false
topBarState.value = false
}
}
com.google.accompanist.insets.ui.Scaffold(
bottomBar = {
BottomBar(
navController = navController,
bottomBarState = bottomBarState
)
},
topBar = {
TopBar(
navController = navController,
topBarState = topBarState
)
},
content = {
NavHost(
navController = navController,
startDestination = NavigationItem.Cars.route,
) {
composable(NavigationItem.Cars.route) {
CarsScreen(
navController = navController,
)
}
composable(NavigationItem.Bikes.route) {
BikesScreen(
navController = navController
)
}
composable(NavigationItem.Settings.route) {
SettingsScreen(
navController = navController,
)
}
composable(NavigationItem.CarDetails.route) {
CarDetailsScreen(
navController = navController,
)
}
}
}
)
Important: Scaffold from Accompanist, initialized in build.gradle. We use Scaffold from Accompanist, because we need full control of paddings, for example in default Scaffold from Compose we can't disable padding for content from top if we have TopAppBar. In our case it's required because we have animation for TopAppBar, content should be under TopAppBar and we manually control padding for each pages. Documentation from Accompanist: https://google.github.io/accompanist/insets/.
Put BottomNavigation inside AnimatedVisibility, set visible value from bottomBarState and set enter and exit animation, in my case I use slideInVertically for enter animation and slideOutVertically for exit animation:
AnimatedVisibility(
visible = bottomBarState.value,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
content = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon),
contentDescription = item.title
)
},
label = { Text(text = item.title) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
)
Put TopAppBar inside AnimatedVisibility, set visible value from topBarState and set enter and exit animation, in my case I use slideInVertically for enter animation and slideOutVertically for exit animation:
AnimatedVisibility(
visible = topBarState.value,
enter = slideInVertically(initialOffsetY = { -it }),
exit = slideOutVertically(targetOffsetY = { -it }),
content = {
TopAppBar(
title = { Text(text = title) },
)
}
)
Full code of MainActivity:
package codes.andreirozov.bottombaranimation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import codes.andreirozov.bottombaranimation.screens.BikesScreen
import codes.andreirozov.bottombaranimation.screens.CarDetailsScreen
import codes.andreirozov.bottombaranimation.screens.CarsScreen
import codes.andreirozov.bottombaranimation.screens.SettingsScreen
import codes.andreirozov.bottombaranimation.ui.theme.BottomBarAnimationTheme
#ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BottomBarAnimationApp()
}
}
}
#ExperimentalAnimationApi
#Composable
fun BottomBarAnimationApp() {
// State of bottomBar, set state to false, if current page route is "car_details"
val bottomBarState = rememberSaveable { (mutableStateOf(true)) }
// State of topBar, set state to false, if current page route is "car_details"
val topBarState = rememberSaveable { (mutableStateOf(true)) }
BottomBarAnimationTheme {
val navController = rememberNavController()
// Subscribe to navBackStackEntry, required to get current route
val navBackStackEntry by navController.currentBackStackEntryAsState()
// Control TopBar and BottomBar
when (navBackStackEntry?.destination?.route) {
"cars" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"bikes" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"settings" -> {
// Show BottomBar and TopBar
bottomBarState.value = true
topBarState.value = true
}
"car_details" -> {
// Hide BottomBar and TopBar
bottomBarState.value = false
topBarState.value = false
}
}
// IMPORTANT, Scaffold from Accompanist, initialized in build.gradle.
// We use Scaffold from Accompanist, because we need full control of paddings, for example
// in default Scaffold from Compose we can't disable padding for content from top if we
// have TopAppBar. In our case it's required because we have animation for TopAppBar,
// content should be under TopAppBar and we manually control padding for each pages.
com.google.accompanist.insets.ui.Scaffold(
bottomBar = {
BottomBar(
navController = navController,
bottomBarState = bottomBarState
)
},
topBar = {
TopBar(
navController = navController,
topBarState = topBarState
)
},
content = {
NavHost(
navController = navController,
startDestination = NavigationItem.Cars.route,
) {
composable(NavigationItem.Cars.route) {
// show BottomBar and TopBar
LaunchedEffect(Unit) {
bottomBarState.value = true
topBarState.value = true
}
CarsScreen(
navController = navController,
)
}
composable(NavigationItem.Bikes.route) {
// show BottomBar and TopBar
LaunchedEffect(Unit) {
bottomBarState.value = true
topBarState.value = true
}
BikesScreen(
navController = navController
)
}
composable(NavigationItem.Settings.route) {
// show BottomBar and TopBar
LaunchedEffect(Unit) {
bottomBarState.value = true
topBarState.value = true
}
SettingsScreen(
navController = navController,
)
}
composable(NavigationItem.CarDetails.route) {
// hide BottomBar and TopBar
LaunchedEffect(Unit) {
bottomBarState.value = false
topBarState.value = false
}
CarDetailsScreen(
navController = navController,
)
}
}
}
)
}
}
#ExperimentalAnimationApi
#Composable
fun BottomBar(navController: NavController, bottomBarState: MutableState<Boolean>) {
val items = listOf(
NavigationItem.Cars,
NavigationItem.Bikes,
NavigationItem.Settings
)
AnimatedVisibility(
visible = bottomBarState.value,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
content = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon),
contentDescription = item.title
)
},
label = { Text(text = item.title) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
)
}
#ExperimentalAnimationApi
#Composable
fun TopBar(navController: NavController, topBarState: MutableState<Boolean>) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val title: String = when (navBackStackEntry?.destination?.route ?: "cars") {
"cars" -> "Cars"
"bikes" -> "Bikes"
"settings" -> "Settings"
"car_details" -> "Cars"
else -> "Cars"
}
AnimatedVisibility(
visible = topBarState.value,
enter = slideInVertically(initialOffsetY = { -it }),
exit = slideOutVertically(targetOffsetY = { -it }),
content = {
TopAppBar(
title = { Text(text = title) },
)
}
)
}
Result:
Don't forget to use #ExperimentalAnimationApi annotation for compose functions.
Update: with Compose version 1.1.0 and above #ExperimentalAnimationApi not required.
22.02.2022 Update: I made some research, and update point 2. Now we use when for control topBarState and bottomBarState.
Full code available on gitHub:
https://github.com/AndreiRoze/BottomBarAnimation/tree/with_animated_topbar
Examples of animations available in the official documentation:
https://developer.android.com/jetpack/compose/animation
for now, I can achieve that by checking current route to show or hide bottomBar, topBar. But I think there's must be better solutions. The way I wrap all screens inside Scaffold might not right.
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
Scaffold(
bottomBar = {
if (currentRoute(navController) != "Example Screen") {
AppBottomBar(navController)
}
},
topBar = {
AppTopBar(scaffoldState)
},
drawerContent = {
DrawerContent(navController, scaffoldState)
},
floatingActionButton = {
FloatingButton(navController)
},
scaffoldState = scaffoldState
) {
// ovoid bottom bar overlay content
Column(modifier = Modifier.padding(bottom = 58.dp)) {
AppNavigation(navController)
}
}
#Composable
public fun currentRoute(navController: NavHostController): String? {
val navBackStackEntry by navController.currentBackStackEntryAsState()
return navBackStackEntry?.arguments?.getString(KEY_ROUTE)
}
Easiest solution I found (without animation)
fun MainScreen(modifier: Modifier = Modifier) {
val navController = rememberNavController()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
showBottomBar = when (navBackStackEntry?.destination?.route) {
"RouteOfScreenA" -> false // on this screen bottom bar should be hidden
"RouteOfScreenB" -> false // here too
else -> true // in all other cases show bottom bar
}
Scaffold(
modifier = modifier,
bottomBar = { if (showBottomBar) MyBottomNavigation(navController = navController) }
) { innerPadding ->
MyNavHost(
navController = navController,
modifier = Modifier.padding(innerPadding)
)
}
}
This worked for me. You get the current route from the currentRoute function and do a check in your bottombar composable to either hide or show the BottomNavigationView.
#Composable
fun currentRoute(navController: NavHostController): String? {
val navBackStackEntry by navController.currentBackStackEntryAsState()
return navBackStackEntry?.destination?.route
}
#Composable
fun MainScreenView() {
val navController = rememberNavController()
Scaffold(bottomBar = {
if (currentRoute(navController) != BottomNavItem.Setup.screen_route)
BottomNavigation(navController = navController)
}
) {
NavigationGraph(navController = navController)
}
}
you can use compositionlocal, and as you wrap your mainActivity with the CompositionLocalProvider, pass the supportActionBar to your child composable At the screen where you wish to hide the topBar, invoke the .hide() method
at the top. See below:
data class ShowAppBar(val show: ActionBar?)
internal val LocalAppBar = compositionLocalOf<ShowAppBar>{ error("No ActionBar provided") }
In mainActivity, pass the ActionBar via
val showy = ShowAppBar(show = supportActionBar )
.....
CompositionLocalProvider(
LocalAppBar provides showy
) {
YourTheme {
yourApp()
}
}
Invoking at the screen >> LocalAppBar.current.show?.hide()

Scaffold with TopAppBar integration with Navigation

How to show navigation icon (BackArrow or Menu) in TopAppBar using Scaffold based on actual position in NavController? I am using Navigating with Compose 1.0.0-alpha02. Below is a sample code with a description of how it should work
#Composable
fun App()
{
val navController = rememberNavController()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "App title") },
navigationIcon = {
/*
Check if navController back stack has more
than one element. If so show BackButton.
Clicking on that button will move back
*/
val canMoveBack = true
if (canMoveBack)
{
IconButton(onClick = {
// Move back
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
// show NavDrawer
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
bodyContent = {
AppBody(navController)
}
)
}
I thought about something like navController.backStack.size but I got error NavController.getBackStack can only be called from within the same library group (groupId=androidx.navigation).
And the second question, if I wanted to change the TopAppBar text do I have to hoist this text and give every "screen" possibility to change this text, or is there any easy built-in way to do this like in the standard View System?
Thanks to Abdelilah El Aissaoui I have got an idea of how to do it with one Scaffold and just changing bodyContent. In this method, we don't have to pass navController to any body element, everything is done in base App composable. Below is code which enables to navigate between two bodies (Lesson -> Student)
App:
#Composable
fun App(
viewModel: MainViewModel
)
{
val navController = rememberNavController()
val baseTitle = "" // stringResource(id = R.string.app_name)
val (title, setTitle) = remember { mutableStateOf(baseTitle) }
val (canPop, setCanPop) = remember { mutableStateOf(false) }
val scaffoldState: ScaffoldState = rememberScaffoldState()
navController.addOnDestinationChangedListener { controller, _, _ ->
setCanPop(controller.previousBackStackEntry != null)
}
// check navigation state and navigate
if (viewModel.navigateToStudents.value)
{
navController.navigate(route = STUDENT_SCREEN_ROUTE)
viewModel.studentsNavigated()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
if (canPop)
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
scaffoldState.drawerState.open()
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
scaffoldState = scaffoldState,
drawerContent = {
DrawerContent()
},
bodyContent = {
AppBody(
viewModel = viewModel,
navController = navController,
setTitle = setTitle
)
}
)
}
AppBody
#Composable
fun AppBody(
viewModel: MainViewModel,
navController: NavHostController,
setTitle: (String) -> Unit,
)
{
NavHost(
navController,
startDestination = LESSON_SCREEN_ROUTE
) {
composable(route = LESSON_SCREEN_ROUTE) {
LessonBody(
viewModel = viewModel,
setTitle = setTitle
)
}
composable(
route = STUDENT_SCREEN_ROUTE
) {
StudentBody(
viewModel = viewModel,
setTitle = setTitle
)
}
}
}
In the ViewModel I use this pattern to navigate:
private val _navigateToStudents: MutableState<Boolean> = mutableStateOf(false)
val navigateToStudents: State<Boolean> = _navigateToStudents
fun studentsNavigated()
{
// here we can add any logic after doing navigation
_navigateToStudents.value = false
}
So when I want to navigate to the next fragment I just set _navigateToStudents.value = true
I was just trying to achieve exactly the same today. I think this code will answer both questions:
#Composable
fun NavigationScaffold(
title: String? = null,
navController: NavController? = null,
bodyContent: #Composable (PaddingValues) -> Unit
) {
val navigationIcon: (#Composable () -> Unit)? =
if (navController?.previousBackStackEntry != null) {
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.Filled.ArrowBack)
}
}
} else {
// this can be null or another component
// If null, the navigationIcon won't be rendered at all
null
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = title.orEmpty())
},
navigationIcon = navigationIcon,
)
},
bodyContent = bodyContent
)
}
As you can see, you can provide a title as a String so you don't have to worry about passing a Text composable.
This Scaffold is the base of all my screens and to use it a simply have to write something like that:
#Composable
fun Home(navController: NavController) {
NavigationScaffold(
title = "Home",
navController = navController
) {
// Screen's content...
}
}

Categories

Resources