How to set title and buttons in TopAppBar of child screens in Android Jetpack Compose

1.9k Views Asked by At

I have an app that is using Top App Bar and bottom navigation. Each bottom tab has its own navigation with NavHostController. On my Home tab, I have a NavHostController like so


    fun HomeNavHostScreen(
        modifier: Modifier = Modifier,
        viewModel: HomeViewModel = hiltViewModel()
    ) {
    
        val title = stringResource(id = R.string.home)
        val navHostController: NavHostController = rememberNavController()
    
    Scaffold(
            topBar = {
                B2CTopAppBar(
                    title = title,
                    navHostController = navHostController
                )
            },
            content = {
                NavHost(
                    navHostController,
                    startDestination = HomeRoute.HomeNavHostScreen.route,
                    modifier = Modifier.padding(top = it.calculateTopPadding())
                ) {
    
                    composable(HomeRoute.HomeNavHostScreen.route) {
                        HomeScreen()
                    }
    
    composable(HomeRoute.DetailScreen.route) {
                        DetailScreen()
                    }
    
    composable(HomeRoute.SomeOtherScreen.route) {
                        SomeScreen()
                    }
    
    }
   }
  )
}

I set the title of Home Screen here, but for my detail screen I want to change the title to something else and add buttons to the TopAppBar for each child screen (DetailScreen, etc)

I tried to remove Scaffold from here and add it to each of my views separately To HomeScreen, Detail etc. But after i found that its not correct to have more than 1 Scaffold per navigation stack.

I am new to Android and Jetpack Compose. In iOS you have access to navigationController and can set titles and buttons from all child views. But I can't seem to find a way to do it here. Thanks in advance for any help.

2

There are 2 best solutions below

0
Dave Kababyan On

So I found a solution that works. Below is what I did:

1st I have created a custom Top App Bar (I'm adding action button as well)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun B2CTopBarWithActionButton(
    title: String,
    action: @Composable (RowScope.() -> Unit),
    navHostController: NavHostController
) {

    val backStackEntry by navHostController.previousBackStackEntryAsState()

    TopAppBar(
        title = { Text(title) },
        actions = action,
        navigationIcon = {
            if (backStackEntry != null) {
                BackButton { navHostController.navigateUp() }
            }
        }
    )
}

Also, I have created a NavHost Screen that takes care of the Navigation. This Screen has a ViewModel as well "TopBarViewModel".

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun HomeNavHostScreen(
    modifier: Modifier = Modifier,
    viewModel: TopBarViewModel = hiltViewModel(),
    navHostController: NavHostController = rememberNavController()
    ) {

    val title by viewModel.navBarTitle.collectAsState()

    Scaffold(
        topBar = {
            B2CTopBarWithActionButton(
                    title = title,
                    navHostController = navHostController,
                    action = {
                        //called if your action button is pressed "If you have button on nav bar"
                    }
                )
        },
        content = {
            NavHost(
                navHostController,
                startDestination = HomeRoute.HomeNavHostScreen.route,
                modifier = Modifier.padding(top = it.calculateTopPadding())
                ) {

                composable("Home") {
                    viewModel.setNavBarTitle("Home")

                    HomeScreen()

                }

                composable("Product") {
                    viewModel.setNavBarTitle("Product")

                    ProductScreen()

                }

            }
        }

        )
}



@OptIn(FlowPreview::class)
@HiltViewModel
class TopBarViewModel @Inject constructor() : ViewModel() {

    private val _navBarTitle = MutableStateFlow("")
    val navBarTitle = _navBarTitle.asStateFlow()

        fun setNavBarTitle(text: String) {
        _navBarTitle.value = text
    }

}

So Basically I am changing the Title in VM every time I navigate to said view.

2
Glaucus On

I ended up solving this as well. My approach was slightly different as I made use of the NavController's currentBackStackEntryFlow. To make use of it however you need to use the LaunchedEffect() function.

I made a composable called AppBar that contains the TopAppBar and updates the title based on the route. To translate the route to a more user friendly title I created a helper function that translates the route to the title.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBar(navController: NavHostController) {
    var title by remember { mutableStateOf("") }

    LaunchedEffect(navController.currentBackStackEntryFlow) {
        navController.currentBackStackEntryFlow.collect {
            title = it.destination.route ?: ""
        }
    }

    Surface(shadowElevation = 3.dp) {
        TopAppBar(
            title = {
                Text(
                    getTitleForRoute(title).uppercase(),
                    style = MaterialTheme.typography.titleLarge
                )
            },
            modifier = Modifier,
            colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = MaterialTheme.colorScheme.surface)
        )
    }
}

Here's the helper function (routes and titles renamed for the sake of clarity), you don't need this but I suspect you'd do something similar:

private fun getTitleForRoute(route: String): String {
    return when (route) {
        "route0" -> "Home"
        "route1" -> "Details"
        "route2" -> "Profile"
        "route3" -> "About"
        else -> ""
    }
}

You probably don't want literal strings for routes here either, but you get the idea.

The advantage here is that you don't need a viewmodel which I think is desirable in this case. That's not to say a viewmodel is a bad idea, but this does reduce the complexity and perhaps keeps it a little cleaner.