Search code examples
androidkotlinandroid-jetpack-navigationandroid-jetpack-compose

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?


Solution

  • 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