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")
}
Related
I am currently using a navigation drawer in jetpack compose, but I have encountered a problem. When I added the navigation drawer to the mainDrawerScreen, it was displaying for every screen I added, in my case it said "Main Menu" for every screen composable when navigated to. This is not ideal.
I decided to remove TopAppBar from mainDrawerScreen and create a seperat scaffold with the TopAppBar for each composable screen. However, now the navigation does not work. I tried to create a trailing lambda for navigation but might have done it the wrong way. Anyone got a suggestion to why it is not opening the navDrawer ? Appreciate the feedback!
MainDrawerSCreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun MainDrawerScreen() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
val navController = rememberNavController()
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
DrawerHeader()
DrawerLayout(scope = scope, scaffoldState = scaffoldState , navController = navController)
}
) {
Navigation(navController = navController) // TODO This controls the navigation between different screens
}
}
My seperate created TopAppBar:
#Composable
fun TopAppBar1(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
text: String,
onIconClick: () -> Unit
) {
TopAppBar(
title = { Text(text = text, fontSize = 18.sp) },
navigationIcon = {
IconButton(onClick = {
scope.launch {
withContext(Dispatchers.IO) { // should I keep this coroutine on background thread ?
scaffoldState.drawerState.open()
}
}
}) {
Icon(
Icons.Filled.Menu, "Menu",
Modifier.clickable { onIconClick.invoke()})
}
},
backgroundColor = LightBlue,
contentColor = MaterialTheme.colors.onPrimary,
)
}
ProfileScreen:
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun ProfileScreen1() {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar1(scope = scope, scaffoldState = scaffoldState, text = "Profile") {
scope.launch { scaffoldState.drawerState.currentValue } <----------- Trailing Lambda
}
},
content = {
ActualBackground() // Background
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { // Content }
I show you what I did :
TopAppBar.kt :
#Composable
fun TopAppBarScreen(
title: String = "",
screen: String,
navController: NavController
) {
TopAppBar(
title = {
Text(
text = title,
style = Typography.h2
)
},
navigationIcon = {
IconButton(
onClick = {
navController.navigate(screen)
}
) {
Icon(Icons.Filled.ArrowBack, "backIcon")
}
},
backgroundColor = Color.Background,
contentColor = Color.BackgroundDarkGrey,
elevation = 0.dp,
)
}
Navigation.kt
#Composable
fun Navigation() {
val navController = rememberNavController()
val viewModel = hiltViewModel<CountrySelectorViewModel>()
val userViewModel = InputViewModel(LocalContext.current)
NavHost(
navController = navController,
startDestination = Screen.WELCOME_SCREEN
) {
composable(Screen.WELCOME_SCREEN) {
WelcomeScreen(navController = navController)
}
}
Screen :
object Screen {
const val WELCOME_SCREEN = "WelcomeScreen"
}
When I call my TopAppBar :
TopAppBarScreen(
screen = Screen.WELCOME_SCREEN,
navController = navController
)
Look at how the navigation works and it's the same if you are using a button to change screen.
I hope this can help you
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"
}
I'm creating a simple app with bottom navigation bar. I want to hide bottom navigation bar while scrolling down and show it while scrolling up in a composable screen.
Any help would be appreciated. Please do let me know if you need any more code. I have attached all the code that I think are relevant to this problem.
This is my bottom navigation bar.
#Composable
fun BottomBar(navController: NavController) {
val items = listOf(
NavigationItem.Home,
NavigationItem.Search
)
BottomNavigation(
backgroundColor = MaterialTheme.colors.DarkRed,
contentColor = Color.White
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
selected = currentRoute == item.route,
icon = {
Icon(
imageVector = item.icon,
contentDescription = "Icon",
modifier = Modifier.size(28.dp)
)
},
alwaysShowLabel = false,
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(0.4f),
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
}
}
)
}
}
}
This is my Main Screen
#Composable
fun MainScreen(){
val navController = rememberNavController()
Scaffold(topBar = {
ActionBar("Books")
},
bottomBar = {
BottomBar(navController)
}){
NavigationGraph(navController = navController)
}
}
And this NavigationGraph
#Composable
fun NavigationGraph(navController: NavHostController){
NavHost(navController = navController, startDestination = NavigationItem.Home.route ){
composable(NavigationItem.Home.route){
HomeScreen()
}
composable(NavigationItem.Search.route){
SearchScreen()
}
}
}
You can use a LazyListState to track the state of the list and only show the BottomBar when the scrollState index is initial. This can be done by:
For LazyColumn:
#Composable
fun HomeScreen(scrollState: LazyListState) {
LazyColumn(
state = scrollState,
) {
...
}
}
For NavigationGraph:
#Composable
fun NavigationGraph(navController: NavHostController, scrollState: LazyListState){
NavHost(navController = navController, startDestination = NavigationItem.Home.route ){
composable(NavigationItem.Home.route){
HomeScreen(scrollState = scrollState)
}
composable(NavigationItem.Search.route){
SearchScreen()
}
}
}
In the MainScreen:
#Composable
fun MainScreen(){
val navController = rememberNavController()
val scrollState = rememberLazyListState()
Scaffold(topBar = {
ActionBar("Books")
},
bottomBar = {
if(scrollState.firstVisibleItemIndex == 0){
BottomBar(navController)
}
}){
NavigationGraph(navController = navController, scrollState = scrollState)
}
}
This way the BottomBar will only show up when the list is at top.
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()
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...
}
}