TopAppBar flashing when navigating with Compose Navigation
F

10

19

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?

enter image description here

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)
        )
    }
}
Filicide answered 3/8, 2021 at 9:40 Comment(0)
F
8

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,
    ) {
        [...}
    }
Filicide answered 8/12, 2021 at 12:10 Comment(3)
this is not a solution. it fixes the toolbar flashing but you break the animation transition for all screens... the solution should not effect other logicHershel
@Hershel You're right. I removed my accepted solutionFilicide
The proper way is to override the default animations and do that in the Navhost composable (exitTransition, popExitTransition, etc.), not to install another library.Hardden
P
8

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.

Palmary answered 15/4, 2022 at 22:17 Comment(3)
Did it you see it somewhere? Mb some official samples?Hershel
can you suggest how that is working in the open source project ivy wallet link - github.com/Ivy-Apps/ivy-walletTonitonia
Worked for me. Could someone please explains why this works? :)Gonophore
I
1

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.

Ibo answered 21/8, 2021 at 13:24 Comment(3)
Right, because they haven't implemented the default transition animation there yet.Filicide
You're right, so... if it's alright to use multiple scaffolds and we don't want the default animation, AnimatedNavHost looks like a way to go?Upthrust
Poplawski Right, looks like this is what we need 👍Filicide
B
1

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

Bacchanalia answered 1/2, 2022 at 14:44 Comment(0)
F
1

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

Frodin answered 27/1, 2023 at 10:14 Comment(0)
B
1

The problem is that the view in the NavHost has a default crossfade animation. You must override it to stop the flashing, as the example below shows with noEnterTransition and noExistTransition for MainScreen.

class MainActivity : ComponentActivity() {
    private val noEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
        {
            fadeIn(
                animationSpec = tween(durationMillis = 200),
                initialAlpha = 0.99f
            )
        }

    private val noExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
        {
            fadeOut(
                animationSpec = tween(durationMillis = 300),
                targetAlpha = 0.99f
            )
        }

    /.../

    override fun onCreate(savedInstanceState: Bundle?) {
        setContent {
            val navController = rememberNavController()
            YourTheme() {

                Surface(color = MaterialTheme.colorScheme.background) {
                    NavHost(navController = navController, startDestination = "main") {
                        composable(
                            route = "main",
                            popEnterTransition = noEnterTransition,
                            exitTransition = noExitTransition,
                            popExitTransition = noExitTransition
                        ) {
                            // MainScreen(navController = navController)
                        }

                        composable(
                            route = "detail",
                            enterTransition = {
                                slideIntoContainer(
                                    towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
                                    animationSpec = tween(200, easing = EaseOut)
                                )
                            },
                            popExitTransition = {
                                slideOutOfContainer(
                                    towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
                                    animationSpec = tween(150, easing = EaseOut)
                                )
                            },
                        ) {
                            // DetailScreen(navController = navController)
                        }
                    }
                }
            }
        }
    }
}
Blodgett answered 28/9, 2023 at 12:58 Comment(1)
This was the only solution that helped me. Thanks!Whitebeam
B
0

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)
    ) {}
Bacchanalia answered 1/2, 2022 at 14:52 Comment(0)
A
0

Without adding another package, as of Dec/2023, you can remove the default animation like so

NavHost(
    navController = navController,
    startDestination = HOME_ROUTE,
    route = ROOT_ROUTE,
    enterTransition = {
        EnterTransition.None // removes the blinking/fade effect
    },
    exitTransition = {
        ExitTransition.None // remove the blinking/fade effect
    }
) {
    // composables
}

Put this in your root nav host. Here is the version that I am using(as of Dec/2/2023

implementation("androidx.navigation:navigation-compose:2.7.5")
Albeit answered 2/12, 2023 at 13:4 Comment(1)
Work for me, easy solution. And don't need for accompanist lib. ThanksAcceptance
G
0

Updating to version 2.8.0-alpha06 solved this issue for me

Galven answered 15/4 at 9:15 Comment(0)
M
-1

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

Mimamsa answered 3/8, 2021 at 11:33 Comment(15)
Having separate toolbars for separate screens was always a viable option in the old fragment times. Especially when you need different kinds of toolbars (like a normal one and an expandable one). What makes you think that now there should always be one single toolbar for all screens?Filicide
There are a lot of things that differ in Jetpack compose compared to the view world. Like nested layouts were a problem in the view system while they are not in compose. I guess they render in different ways so that could be it. Check the docs for not maybeMimamsa
I'm not sold on this. A single Scaffold/AppBar per app seems like a nightmare to manageFilicide
It's only jarring because of the crossfade, it would not be an issue if there was slide animation supportIrvin
Good point. Also, if there WAS only one app barMimamsa
After checking some other apps and official samples by Google, your assumption that there should only be a single scaffold/topbar in Compose seems to be wrong.Filicide
Oh yeah? [][][]Mimamsa
@FlorianWalther Could you please let me know which Google sample uses multiple scaffolds? I could not find it.Whinchat
I'm convinced this is the way, but do you have any examples?Hinz
@Whinchat Jetsurvey: github.com/android/compose-samples/blob/…Garnet
Yeah most of the apps use this method so you could almost blindly pick up any app on the web (official apps, preferably, since they will always incorporate best practices, or at least won't follow bad practices).Mimamsa
hey @FlorianWalther were you able to get over the flashing animation? OR did you have to use one toolbar? If one toolbar, how did you change the toolbar info from each screen?Fulminant
@Fulminant I ended up using Accompanist to disable the default crossfade animation (which causes the flash effect)Filicide
Thanks @FlorianWalther . It may worth answering your own questions with an example for other people who gets same issue :)Fulminant
@Fulminant yea I'll do that thatFilicide

© 2022 - 2024 — McMap. All rights reserved.