TopAppBar flashing when navigating with Compose Navigation - android

I have 2 screens which both have their own Scaffold and TopAppBar. When I navigate between them using the Jetpack Navigation Compose library, the app bar flashes. Why does it happen and how can I get rid of this?
Code:
Navigation:
#Composable
fun TodoNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = TodoScreen.TodoList.name,
modifier = modifier
) {
composable(TodoScreen.TodoList.name) {
TodoListScreen(
onTodoEditClicked = { todo ->
navController.navigate("${TodoScreen.AddEditTodo.name}?todoId=${todo.id}")
},
onFabAddNewTodoClicked = {
navController.navigate(TodoScreen.AddEditTodo.name)
}
)
}
composable(
"${TodoScreen.AddEditTodo.name}?todoId={todoId}",
arguments = listOf(
navArgument("todoId") {
type = NavType.LongType
defaultValue = -1L
}
)
) {
AddEditTodoScreen(
onNavigateUp = {
navController.popBackStack()
},
onNavigateBackWithResult = { result ->
navController.navigate(TodoScreen.TodoList.name)
}
)
}
}
}
Todo list screen Scaffold with TopAppBar:
#Composable
fun TodoListBody(
todos: List<Todo>,
todoExpandedStates: Map<Long, Boolean>,
onTodoItemClicked: (Todo) -> Unit,
onTodoCheckedChanged: (Todo, Boolean) -> Unit,
onTodoEditClicked: (Todo) -> Unit,
onFabAddNewTodoClicked: () -> Unit,
onDeleteAllCompletedConfirmed: () -> Unit,
modifier: Modifier = Modifier,
errorSnackbarMessage: String = "",
errorSnackbarShown: Boolean = false
) {
var menuExpanded by remember { mutableStateOf(false) }
var showDeleteAllCompletedConfirmationDialog by rememberSaveable { mutableStateOf(false) }
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text("My Todos") },
actions = {
IconButton(
onClick = { menuExpanded = !menuExpanded },
modifier = Modifier.semantics {
contentDescription = "Options Menu"
}
) {
Icon(Icons.Default.MoreVert, contentDescription = "Show menu")
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(
onClick = {
showDeleteAllCompletedConfirmationDialog = true
menuExpanded = false
},
modifier = Modifier.semantics {
contentDescription = "Option Delete All Completed"
}) {
Text("Delete all completed")
}
}
}
)
},
[...]
Add/edit screen Scaffold with TopAppBar:
#Composable
fun AddEditTodoBody(
todo: Todo?,
todoTitle: String,
setTitle: (String) -> Unit,
todoImportance: Boolean,
setImportance: (Boolean) -> Unit,
onSaveClick: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text(todo?.let { "Edit Todo" } ?: "Add Todo") },
actions = {
IconButton(onClick = onSaveClick) {
Icon(Icons.Default.Save, contentDescription = "Save Todo")
}
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
},
) { innerPadding ->
BodyContent(
todoTitle = todoTitle,
setTitle = setTitle,
todoImportance = todoImportance,
setImportance = setImportance,
modifier = Modifier.padding(innerPadding)
)
}
}

The flashing is caused by the default cross-fade animation in more recent versions of the navigation-compose library. The only way to get rid of it right now (without downgrading the dependency) is by using the Accompanist animation library:
implementation "com.google.accompanist:accompanist-navigation-animation:0.20.0"
And then replace the normal NavHost with Accompanist's AnimatedNavHost, replace rememberNavController() with rememberAnimatedNavController() and disable the transitions animations:
AnimatedNavHost(
navController = navController,
startDestination = bottomNavDestinations[0].fullRoute,
enterTransition = { _, _ -> EnterTransition.None },
exitTransition = { _, _ -> ExitTransition.None },
popEnterTransition = { _, _ -> EnterTransition.None },
popExitTransition = { _, _ -> ExitTransition.None },
modifier = modifier,
) {
[...}
}

I think I found an easy solution for that issue (works on Compose version 1.4.0).
My setup - blinking
All of my screens have their own toolbar wrapped in the scaffold:
// Some Composable screnn
Scaffold(
topBar = { TopAppBar(...) }
) {
ScreenContent()
}
Main activity which holds the nav host is defined like that:
// Activity with NavHost
setContent {
AppTheme {
NavHost(...) { }
}
}
Solution - no blinking!
Wrap you NavHost in activity in a Surface:
setContent {
AppTheme {
Surface {
NavHost(...) { }
}
}
}
Rest of the screens stay the same. No blinking and transition animation between destinations is almost the same like it was with fragments (subtle fade in/fade out). So far I haven't found any negative side effects of that.

It is the expected behaviour. You are constructing two separate app bars for both the screens so they are bound to flash. This is not the correct way. The correct way would be to actually put the scaffold in your main activity and place the NavHost as it's content. If you wish to modify the app bar, create variables to hold state. Then modify them from the Composables. Ideally, store then in a viewmodel. That is how it is done in compose. Through variables.
Thanks

I got the same issue having a "scaffold-per-screen" architecture. What helped, to my surprise, was lowering androidx.navigation:navigation-compose version to 2.4.0-alpha04.

With the newer library implementation "com.google.accompanist:accompanist-navigation-animation:0.24.1-alpha"
you need to have the AnimatedNavHost like this
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None },
modifier = Modifier.padding(innerPadding)
Also
Replace rememberNavController() with rememberAnimatedNavController()
Replace NavHost with AnimatedNavHost
Replace import androidx.navigation.compose.navigation with import com.google.accompanist.navigation.animation.navigation
Replace import androidx.navigation.compose.composable with import com.google.accompanist.navigation.animation.composable

In order not to blink (or to slide if you have AnimatedNavHost) you should put the TopAppBar in the activity and outside the NavHost, otherwise the TopAppBar is just part of the screen and makes transitions like every other screen element:
// Activity with your navigation host
setContent {
MyAppTheme {
Scaffold(
topBar = { TopAppBar(...) }
) { padding ->
TodoNavHost(padding, ...) { }
}
}
}
From the Scaffold containing the TopAppBar comes the padding parameter, that you should pass to the NavHost and use it in the screen like you have done in your example

Alternative to removing Animation you can change animations for example:
#Composable
private fun ScreenContent() {
val navController = rememberAnimatedNavController()
val springSpec = spring<IntOffset>(dampingRatio = Spring.DampingRatioMediumBouncy)
val tweenSpec = tween<IntOffset>(durationMillis = 2000, easing = CubicBezierEasing(0.08f, 0.93f, 0.68f, 1.27f))
...
) { innerPadding ->
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = springSpec) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = springSpec) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = tweenSpec) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = tweenSpec) },
modifier = Modifier.padding(innerPadding)
) {}

Related

Compose BottomNavigation padding

Im trying to properly apply a bottom window inset on the BottomNavigation composable. I have an edge to edge scaffold it looks like that:
When using:
modifier.navigationBarsPadding()
On the bottom navigation im getting the following:
I'm trying to achieve the following:
Scaffold(
modifier = Modifier
.background(brush = NanitColors.blueGradient),
topBar = {
topBar()
},
bottomBar = {
if (shouldShowBottomBar) {
BottomNavigationBar(navController)
}
},
scaffoldState = scaffoldState,
drawerContent = {
Drawer { route ->
scope.launch {
scaffoldState.drawerState.close()
}
navController.navigate(route.route) {
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
currentDestination = route
}
},
drawerGesturesEnabled = scaffoldState.drawerState.isOpen,
) { innerPadding ->
NavigationHost(navController = navController, modifier = Modifier.padding(innerPadding))
}
val topBar: #Composable () -> Unit = {
MainToolbar(
modifier = modifier.statusBarsPadding(),
title = title ?: "",
onMenuClicked = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
BottomNavigation(
modifier = modifier.navigationBarsPadding()
) {
items.forEach { item ->
BottomNavigationBarItem(
item = item,
isSelected = selectedItem?.route == item.route,
onSelectedChange = { onSelectedItemChange(item) }
)
}
}
You can use accompanist systemuicontroller to set color to navigation bar, use default color for other screens and add color just for the screen you need:
#Composable
fun AppTheme(
systemBarColor: Color = MaterialTheme.colors.background,
content: #Composable () -> Unit,
) {
...
SideEffect {
systemUiController.setNavigationBarColor(
color = systemBarColor,
darkIcons = useDarkIcons
)
}
}

Jetpack Compose TopAppBar with dynamic actions

#Composable
fun TopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable (() -> Unit)? = null,
actions: #Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
actions: #Composable RowScope.() -> Unit = {}
Usage Scenario:
Using Compose Navigation to switch to different "screens", so the TopAppBar actions will be changed accordingly. Eg. Share buttons for content screen, Filter button for listing screen
Tried passing as a state to the TopAppBar's actions parameter, but having trouble to save the lambda block for the remember function.
val (actions, setActions) = rememberSaveable { mutableStateOf( appBarActions ) }
Want to change the app bar actions content dynamically. Any way to do it?
This the approach I used but I'm pretty new on compose, so I cannot be sure it is the correct approach.
Let's assume I have 2 screens: ScreenA and ScreenB
They are handled by MainActivity screen.
This is our MainActivity:
#ExperimentalComposeUiApi
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CoolDrinksTheme {
val navController = rememberNavController()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var appBarState by remember {
mutableStateOf(AppBarState())
}
Scaffold(
topBar = {
SmallTopAppBar(
title = {
Text(text = appBarState.title)
},
actions = {
appBarState.actions?.invoke(this)
}
)
}
) { values ->
NavHost(
navController = navController,
startDestination = "screen_a",
modifier = Modifier.padding(
values
)
) {
composable("screen_a") {
ScreenA(
onComposing = {
appBarState = it
},
navController = navController
)
}
composable("screen_b") {
ScreenB(
onComposing = {
appBarState = it
},
navController = navController
)
}
}
}
}
}
}
}
}
As you can see I'm using a mutable state of a class which represents the state of our MainActivity (where the TopAppBar is declared and composed), in this example there is the title and the actions of our TopAppBar.
This mutable state is set with a callback function called inside the composition of each screen.
Here you can see the ScreenA
#Composable
fun ScreenA(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen A",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Filter,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen A"
)
Button(onClick = {
navController.navigate("screen_b")
}) {
Text(text = "Navigate to Screen B")
}
}
}
And the ScreenB
#Composable
fun ScreenB(
onComposing: (AppBarState) -> Unit,
navController: NavController
) {
LaunchedEffect(key1 = true) {
onComposing(
AppBarState(
title = "My Screen B",
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null
)
}
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
)
)
}
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Screen B"
)
Button(onClick = {
navController.popBackStack()
}) {
Text(text = "Navigate back to Screen A")
}
}
}
And finally this is the data class of our state:
data class AppBarState(
val title: String = "",
val actions: (#Composable RowScope.() -> Unit)? = null
)
In this way you have a dynamic appbar declared in the main activity but each screen is responsable to handle the content of the appbar.
First you need to add navigation dependency on you jetpack compose projects.
You can read the doc from this https://developer.android.com/jetpack/compose/navigation
def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
Then define your screen in sealed class:
sealed class Screen(var icon: ImageVector, var route: String) {
object ContentScreen: Screen(Icons.Default.Home, "home")
object ListingScreen: Screen(Icons.Default.List, "list")
}
and this is the navigation function look like
#Composable
fun Navigation(paddingValues: PaddingValues, navController: NavHostController) {
NavHost(navController, startDestination = Screen.ContentScreen.route, modifier = Modifier.padding(paddingValues)) {
composable(Screen.ContentScreen.route) {
//your screen content
}
composable(Screen.ListingScreen.route) {
//your listing screen here
}
}
}
Finally in your mainactivity class
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestAppTheme {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "main screen") }, actions = {
if (currentRoute == Screen.ContentScreen.route) {
//your share button action here
} else if (currentRoute == Screen.ListingScreen.route) {
//your filter button here
} else {
//other action
}
})
}
) {
Navigation(paddingValues = it, navController = navController)
}
}
}
}
I'm so sorry if the explanation to sort, because the limitation of my English

Jetpack Compose call function from viewmodel in my bottom bar

I have a Bottom Bar, and in this composable function I want to call a function that i've set up in a ViewModel in my Apps Navigation's Nav Graph, but can't think of any way to do this? I've played with some interfaces however I'm not getting anywhere
#Composable
fun BottomNavBar(
currentRoute: String?,
navigateToBuild: () -> Unit,
navigateToSaved: () -> Unit
) {
Column() {
Row(modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color.Gray)) {
}
BottomAppBar(
modifier = Modifier
.height(72.dp),
backgroundColor = Color.White
) {
navItems.forEach { item ->
val selected = currentRoute == item.route
BottomNavigationItem(
icon = {
Image(
painter = painterResource(
id = if (selected) item.selectedIcon else
item.unselectedIcon),
contentDescription = item.title
)
},
selected = selected,
onClick = {
when (item.route) {
NavigationItem.Build.route -> {
navigateToBuild()
}
NavigationItem.Saved.route -> {
navigateToSaved()
// I want to call viewmodel function here
}
}
}
)
}
}
}
}
My Bottom bar is part of the scaffold here, and my viewmodel is inside the AppNavigation composable, so they are both completely separate and I can't think of any way for them to communicate?
Scaffold(
bottomBar = {
BottomNavBar(
currentRoute = currentRoute,
navigateToBuild = { navController.navigate("build/0") },
navigateToSaved = { navController.navigate(DashboardScreens.Saved.route) })
}
) { innerPadding ->
Box(
modifier = Modifier
.padding(innerPadding)
.background(Color.White)
) {
AppNavigation(navHostController = navController)
}
}
In Compose, the view model lifecycle is bound to the compose navigation route (if there is one) or to Activity / Fragment otherwise, depending on where setContent was called from.
The first viewModel() call creates a view model, all other calls to the same class from any composable function will return the same object to you. So as long as you are inside the same navigation route, you can safely call viewModel() from BottomNavBar:
#Composable
fun BottomNavBar(
currentRoute: String?,
navigateToBuild: () -> Unit,
navigateToSaved: () -> Unit
) {
val viewModel = viewModel<ViewModelClass>()
}
#Composable
fun AppNavigation(navHostController: NavController) {
val viewModel = viewModel<ViewModelClass>()
}

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