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)
)
}
}
I think I found an easy solution for that issue (works on Compose version 1.4.0).
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(...) { }
}
}
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.
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
.
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 elements:
// 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
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
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 replacing the normal NavHost
for Accompanist's AnimatedNavHost
and disabling the transitions animations:
AnimatedNavHost(
navController = navController,
startDestination = bottomNavDestinations[0].fullRoute,
enterTransition = { _, _ -> EnterTransition.None },
exitTransition = { _, _ -> ExitTransition.None },
popEnterTransition = { _, _ -> EnterTransition.None },
popExitTransition = { _, _ -> ExitTransition.None },
modifier = modifier,
) {
[...}
}
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
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)
) {}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.