5

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?

iknow
  • 8,358
  • 12
  • 41
  • 68

2 Answers2

9

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

iknow
  • 8,358
  • 12
  • 41
  • 68
4

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...
    }
}
Abdelilah El Aissaoui
  • 4,204
  • 2
  • 27
  • 47