I was developing an App with Jetpack Compose, and I try to implement the Navigation Compose component.
My use of case is based on 3 Screens without fragments:
List Screen
Detail Screen
Bought Screen
I also implement a BottomTab where I have List Screen and Bought Screen. the Detail Screen is access from List Screen.
BottomNavigation.kt
#Composable
fun BottomNavigationBar(heightBottomBar: Int, navController: NavController) {
Log.d("navigation", "navigatorName: ${navController.currentBackStackEntry?.destination?.navigatorName}")
Log.d("navigation", "displayName: ${navController.currentBackStackEntry?.destination?.displayName}")
Log.d("navigation", "arguments: ${navController.currentBackStackEntry?.arguments}")
val items = listOf(
BottomNavItem.ListScreen,
BottomNavItem.BoughtScreen
)
BottomNavigation(
Modifier.height(heightBottomBar.dp),
backgroundColor = MaterialTheme.colors.background,
contentColor = Color.White)
{
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
label = {
Text(
text = item.title,
fontSize = 9.sp
)
},
selectedContentColor = androidx.compose.ui.graphics.Color.Black,
unselectedContentColor = androidx.compose.ui.graphics.Color.Black.copy(0.4f),
alwaysShowLabel = true,
selected = currentRoute == item.screen_route,
onClick = {
//Added to manage navigate to List screen, instead of detail screen when user pop List tab from BoughtScreen.
// if(item.screen_route == "pokemon_list_screen" && navBackStackEntry?.destination?.parent?.route == "pokemon_list_screen"){
// navController.popBackStack()
// }else{
navController.navigate(item.screen_route) {
navController.graph.startDestinationRoute?.let { screen_route ->
popUpTo(screen_route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
//}
)
}
}
}
And ButtonNav.kt file
sealed class BottomNavItem(var title:String, var icon:Int, var screen_route:String){
object BoughtScreen : BottomNavItem("My Pokemons", R.drawable.ic_all_inbox,"pokemon_bought_screen")
object ListScreen: BottomNavItem("List",R.drawable.ic_list,"pokemon_list_screen")
}
the issue is when I try to navigate from Bought Screen to List screen, pop in the List tab, which take me to detail screen, when it should take to List Screen.
The navigation implementation is the following:
#Composable
fun NavigationGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screens.List.route) {
composable(Screens.List.route) {
PokemonListScreen(navController = navController)
}
composable(
Screens.Detail.route,
// use as URL
//which pokemon you want to see in detail with colour in background pass arguments
arguments = listOf(
navArgument("dominantColor") {
type = NavType.IntType
},
navArgument("pokemonName") {
type = NavType.StringType
},
navArgument("number") {
type = NavType.IntType
}
),
) {
val dominantColor = remember {
val color = it.arguments?.getInt("dominantColor")
color?.let { Color(it) } ?: Color.White
}
val pokemonName = remember {
it.arguments?.getString("pokemonName")
}
val pokemonNumber = remember {
it.arguments?.getInt("number")
}
if (pokemonName != null) {
PokemonDetailScreen(
activity = this#MainActivity,
context = applicationContext,
dominantColor = dominantColor,
pokemonName = pokemonName.toLowerCase(Locale.ROOT) ?: " ",
pokemonNumber = pokemonNumber!!,
navController = navController
)
}
}
composable(Screens.Bought.route) {
MyPokemonsScreen(
context = applicationContext,
dominantColor = MaterialTheme.colors.background,
navController = navController
)
}
}
}
And my Screens.kt file:
sealed class Screens(val route: String) {
object List : Screens("pokemon_list_screen")
object Bought : Screens("pokemon_bought_screen")
object Detail : Screens("pokemon_detail_screen/{dominantColor}/{pokemonName}/{number}")
}
As I comment, with this implementation the apps works fine, except when I navigate to Bought Screen, and I click in the List Tab, the app take me to Detail Screen.
So my question is: Should I modify NavController to remove DetailScreen from currentBackStackEntry, or I should manage this from Bottomavigation::class.
I just start with Jetpack Compose, and I a bit undevelop on this topics.
If you have some knowledge about Jetpack Compose Navigation, and are able to help, take thanks in advance !
Related
I have BottomNavigation wrapped around as a BottomBar composable and inside my MainScreen composable.
The current implementation renders properly and I am able to navigate between different Pages. However, the issue I am facing is that the selected property in the BottomNavigationItem is not been updated correctly when changing the current page. This causes the UI to be located in the "Profile" page, but the bottom bar highlights the "Home" page.
I am almost sure it is something regarding the state of the composable, and I tried passing the NavHostController all the way down to the BottomBar, but that didn't solved the issue.
enum class Page(
#StringRes val title: Int,
val isCorePage: Boolean,
val icon: ImageVector) {
Profile(
title = R.string.profile_page_title,
isCorePage = true,
icon = Icons.Filled.Person),
Home(
title = R.string.home_page_title,
isCorePage = true,
icon = Icons.Filled.Home),
... etc
}
#Composable
fun BottomBar(
currentPage: Page,
onClick: (page: Page) -> Unit = { }) {
BottomNavigation {
Page.values().forEach { page ->
if(page.isCorePage) {
val title = stringResource(page.title);
BottomNavigationItem(
icon = { Icon(
imageVector = page.icon,
contentDescription = title) },
label = { Text(text = title) },
selected = page == currentPage,
onClick = { onClick(page) }
)
}
}
}
}
#Composable
fun MainScreen(
sessionState: State<Session>,
navController: NavHostController = rememberNavController()) {
val backStackEntry by navController.currentBackStackEntryAsState()
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopBar(
title = backStackEntry?.destination?.route ?: "Title",
menuClick = { scope.launch { scaffoldState.drawerState.open() } },
searchClick = { navController.navigate(Page.Search) },
notificationsClick = { navController.navigate(Page.Notifications) })
},
drawerContent = { SideDrawer(sessionState = sessionState) },
bottomBar = {
BottomBar(
currentPage = navController.currentPageOrDefault(Page.Home),
onClick = { page -> navController.navigate(page) })
}
) {
PageNavHost(navController)
}
}
fun NavHostController.navigate(page: Page) {
val navController = this
navController.navigate(page.name) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// re-selecting the same item
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}
}
fun NavHostController.currentPageOrDefault(default: Page): Page {
return this.currentPage() ?: default
}
fun NavHostController.currentPage(): Page? {
val currentRoute = this.currentDestination?.route ?: return null;
Page.values().forEach { page ->
if(page.name == currentRoute) {
return page
}
}
return null
}
I want to implement a BottomNavBar with 3 items.
Each navigating to a new navGraph. (Nested Navigation)
Home = "HomeGraph"
Explore = "ExploreGraph"
Profile = "ProfileGraph"
HomeGraph:
#OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.addHomeGraph(navController: NavHostController) {
navigation(
startDestination = "feed",
route = "HomeGraph"
) {
composable(
route = "feed"
) {
}
composable(
route = "comments"
) {
}
}
}
And my BottomNavBar navigates between those 3 graphs:
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
BottomNavigationItem.HomeGraph,
BottomNavigationItem.ExploreGraph,
BottomNavigationItem.ProfileGraph
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
BottomNavigation() {
Row(horizontalArrangement = Arrangement.Center) {
items.forEach { item ->
BottomNavigationItem(
icon = {
if (currentRoute == item.route) {
Icon(
painterResource(id = item.iconPressed),
contentDescription = item.title
)
} else {
Icon(
painterResource(id = item.iconNormal),
contentDescription = item.title
)
}
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.onSurface,
alwaysShowLabel = false,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
}
Now the problem is that my "Home" BottomNav Icon is not changing cause "selected" is never true.
selected = currentRoute == item.route,
How can I check if I am still on the same navigationGraph?
So when I go down the "HomeGraph" for example to "feed" or "comments" it should still indicate the "Home" Icon on my BottomNavBar as selected.
"feed" and "Comments" have in common, that their parent NavGraph is called "HomeGraph", how can I check for that?
With other words: I want pretty much the same BottomNavBar behaviour as Instagram
Instead of matching navBackStackEntry?.destination?.route, try looking for your route in the NavDestination's hierarchy.
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Then in your forEach:
val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true
I have implemented the accompanist navigation animation library in my project and have stumbled into two issues. The first issue is that the animations aren't being applied when navigating from one screen to another. The second issue is that the "back" of the system closes the app to the background instead of returning to the previous screen.
Here is the layout of the app starting from the MainActivity.
MainActivity.kt
#ExperimentalAnimationApi
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val preDrawListener = ViewTreeObserver.OnPreDrawListener { false }
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.MyHomeTheme)
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(preDrawListener)
lifecycleScope.launch {
setContent {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = true
)
systemUiController.setNavigationBarColor(
color = Color(0x40000000),
darkIcons = false
)
}
MyHomeApp(
currentRoute = Destinations.Welcome.WELCOME_ROUTE
)
}
unblockDrawing()
}
}
private fun unblockDrawing() {
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
content.viewTreeObserver.addOnPreDrawListener { true }
}
}
MyHomeApp.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeApp(currentRoute: String) {
MyHomeTheme {
ProvideWindowInsets {
val navController = rememberAnimatedNavController()
val scaffoldState = rememberScaffoldState()
val darkTheme = isSystemInDarkTheme()
val items = listOf(
HomeTab.Dashboard,
HomeTab.Details,
HomeTab.Settings
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val bottomPaddingModifier = if (currentDestination?.route?.contains("welcome") == true) {
Modifier
} else {
Modifier.navigationBarsPadding()
}
Scaffold(
modifier = Modifier
.fillMaxSize()
.then(bottomPaddingModifier),
scaffoldState = scaffoldState,
bottomBar = {
if (currentDestination?.route in items.map { it.route }) {
BottomNavigation {
items.forEach { screen ->
BottomNavigationItem(
label = { Text(screen.title) },
icon = {},
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
}
) { innerPadding ->
MyHomeNavGraph(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = navBackStackEntry?.destination?.route ?: currentRoute
)
}
}
}
}
sealed class HomeTab(
val route: String,
val title: String
) {
object Dashboard : HomeTab(
route = Destinations.Home.HOME_DASHBOARD,
title = "Dashboard"
)
object Details : HomeTab(
route = Destinations.Home.HOME_DETAILS,
title = "Details"
)
object Settings : HomeTab(
route = Destinations.Home.HOME_SETTINGS,
title = "Settings"
)
}
MyHomeNavGraph.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeNavGraph(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String
) {
val actions = remember(navController) { Actions(navController = navController) }
AnimatedNavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(
route = Destinations.Welcome.WELCOME_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeScreen(
navigateToLogin = actions.navigateToWelcomeLogin,
navigateToRegister = actions.navigateToWelcomeRegister,
)
}
composable(
route = Destinations.Welcome.WELCOME_LOGIN_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeLoginScreen(
// Arguments will be passed to navigate to the home screen or other
)
}
}
}
class Actions(val navController: NavHostController) {
// Welcome
val navigateToWelcome = {
navController.navigate(Destinations.Welcome.WELCOME_ROUTE)
}
val navigateToWelcomeLogin = {
navController.navigate(Destinations.Welcome.WELCOME_LOGIN_ROUTE)
}
}
For simplicity's sake, you can assume that the screens are juste a box with a button in the middle which executes the navigation when they are clicked.
The accompanist version I am using is 0.24.1-alpha (the latest as of this question) and I am using compose version 1.2.0-alpha02 and kotlin 1.6.10.
In terms of animation, the only difference I can see with the accompanist samples is that I don't pass the navController to the screens but I don't see how that could be an issue.
And in terms of using the system back which should return to a previous, I'm genuinely stuck in terms of what could cause the navigation to close the app instead of going back. On other projects, the system back works just fine but not with this one. Is the use of the accompanist navigation incompatible ? I'm not sure.
Any help is appreciated!
I found the source of the issue.
The fact that I was setting the startDestination parameter to navBackStackEntry?.destination?.route ?: currentRoute meant that each change to the navBackStackEntry recomposed the MyHomeNavGraph and hence the backstack was reset upon the recomposition.
Note to self, watch out when copying navigation from multiple sources!
The Code A is from the end branch of the official sample project.
When I click the Tab of the UI, the App will show the corresponding UI, and the current Tab will be marked as a different style.
In Overview.name Tab UI page, if I click a account item, the code composable(Overview.name) {... onAccountClick = { name -> navigateToSingleAccount(navController, name) } will navigate it to composable( route = "$accountsName/{name}", ..) UI , and Accounts.name is marked as current Tab.
What make me confuse is why Android can make Accounts.name as current Tab, could you tell me? Will navController.navigate() cause Android recomposition? so is val currentScreen = RallyScreen.fromRoute(backstackEntry.value?.destination?.route) re-executed ?
Code A
#Composable
fun RallyApp() {
RallyTheme {
val allScreens = RallyScreen.values().toList()
val navController = rememberNavController()
val backstackEntry = navController.currentBackStackEntryAsState()
val currentScreen = RallyScreen.fromRoute(backstackEntry.value?.destination?.route)
Scaffold(
topBar = {
RallyTabRow(
allScreens = allScreens,
onTabSelected = { screen ->
navController.navigate(screen.name)
},
currentScreen = currentScreen
)
}
) { innerPadding ->
RallyNavHost(navController, modifier = Modifier.padding(innerPadding))
}
}
}
#Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = Overview.name,
modifier = modifier
) {
composable(Overview.name) {
OverviewBody(
onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
onClickSeeAllBills = { navController.navigate(Bills.name) },
onAccountClick = { name ->
navigateToSingleAccount(navController, name)
},
)
}
composable(Accounts.name) {
AccountsBody(accounts = UserData.accounts) { name ->
navigateToSingleAccount(navController = navController, accountName = name)
}
}
composable(Bills.name) {
...
}
val accountsName = Accounts.name
composable(
route = "$accountsName/{name}",
arguments = listOf(
navArgument("name") {
type = NavType.StringType
}
),
deepLinks = listOf(
navDeepLink {
uriPattern = "rally://$accountsName/{name}"
}
),
) { entry ->
val accountName = entry.arguments?.getString("name")
val account = UserData.getAccount(accountName)
SingleAccountBody(account = account)
}
}
}
private fun navigateToSingleAccount(navController: NavHostController, accountName: String) {
navController.navigate("${Accounts.name}/$accountName")
}
enum class RallyScreen(
val icon: ImageVector,
) {
Overview(
icon = Icons.Filled.PieChart,
),
Accounts(
icon = Icons.Filled.AttachMoney,
),
Bills(
icon = Icons.Filled.MoneyOff,
);
companion object {
fun fromRoute(route: String?): RallyScreen =
when (route?.substringBefore("/")) {
Accounts.name -> Accounts
Bills.name -> Bills
Overview.name -> Overview
null -> Overview
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
}
}
Yes, navigate() will definitely cause recomposition as the UI is getting re-created.
Yes, val currentScreen = RallyScreen.fromRoute(backstackEntry.value?.destination?.route) will be re-executed as backstackEntry is being observed as state and its value will change when you navigate. That's why the RallyApp composable will also get recomposed. So when it is executed again, currentScreen value will get updated.
You can solve it using Effects as per your requirement. In this scenario, you can use LaunchedEffect. like,
LaunchedEffect(null) {
// code that you don't want to run again.
}
Another approach for now(Which I do not recommend), to avoid your composable code from re-running by checking the current route in the back-stack entry and your expected route. To do that you can use the following piece of code.
#Composable
fun YourComposableScreen(navController: NavController) {
if (yourExpectedRoute != navController.currentBackStackEntry?.destination?.route) {
return
}
// Your other composable(s) goes here.
}
I have a auth page and after the auth page, I basically navigate to a Tabbed application.
The problem is that bottom bar disappears once I click on tab.
Below is what my code looks like
sealed class ScreenM(val route: String) {
object Landing: Screen("landingm")
object Tab: Screen("tabm")
}
sealed class Screen(val route: String) {
object PasswordLogin: Screen("passwordlogin")
object TabBar: Screen("tabbar")
}
sealed class TabbarItem(var route: String, var icon: Int, var title: String) {
object Home : TabbarItem("tabhome", R.drawable.ic_home_tab_icon, "Home")
object Profile : TabbarItem("tabprofile", R.drawable.ic_profile_tab_icon, "Profile")
}
my application entry point is
#Composable
fun App() {
val navController = rememberNavController()
NavHost(navController, startDestination = ScreenM.Landing.route) {
addLandingTopLevel(navController = navController)
addTabBarTopLevel(navController = navController)
}
}
private fun NavGraphBuilder.addLandingTopLevel(
navController: NavController,
) {
navigation(
route = ScreenM.Landing.route,
startDestination = Screen.Home.route
) {
addPasswordLogin(navController)
}
}
private fun NavGraphBuilder.addPasswordLogin(navController: NavController) {
composable(route = Screen.PasswordLogin.route) {
PasswordLoginView(navController)
}
}
private fun NavGraphBuilder.addTabBarTopLevel(
navController: NavController,
) {
navigation(
route = ScreenM.Tab.route,
startDestination = Screen.TabBar.route
) {
addTabBar(navController)
addHome(navController)
addProfile(navController)
}
}
private fun NavGraphBuilder.addTabBar(navController: NavController) {
composable(route = Screen.TabBar.route) {
TabBarView(navController)
}
}
private fun NavGraphBuilder.addHome(navController: NavController) {
composable(route = TabbarItem.Home.route) {
HomeView()
}
}
private fun NavGraphBuilder.addProfile(navController: NavController) {
composable(route = TabbarItem.Profile.route) {
ProfileView()
}
}
I am triggering the Tab like this
// ...
NavigationButton(buttonText = "Login", onBackPressed = {
navController.popBackStack()
}) {
navController.navigate(ScreenM.Tab.route)
}
// ...
Then my Tabbar is like
#Composable
fun TabBarView(navController: NavController) {
Scaffold(
bottomBar = { BottomNavigationBar(navController) }
) {
}
}
Then bottom navigation Bar is like this
#Composable
fun BottomNavigationBar(navController: NavController) {
val items = listOf(
TabbarItem.Home,
TabbarItem.Profile
)
BottomNavigation(
backgroundColor = colorResource(id = R.color.white),
contentColor = Color.Black
) {
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
label = { Text(text = item.title) },
selectedContentColor = Color.Red,
unselectedContentColor = Color.Blue.copy(0.4f),
alwaysShowLabel = true,
selected = false,
onClick = {
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
You have two options.
Displaying BottomNavigationBar inside each tab
1.1. Not sure what's addTabBar navigation route in your code, I don't think it's need, as looks like you only have two tabs: TabbarItem.Home and TabbarItem.Profile.
1.2. You can add BottomNavigationBar inside each view, specifying the selected item. Inside HomeView it can look like this:
BottomNavigationBar(navController, selectedTab = TabbarItem.Home)
1.3. Depending on the selected tab, you need to select the needed item of BottomNavigationBar
#Composable
fun BottomNavigationBar(navController: NavController, selectedTab: TabbarItem) {
val items = listOf(
TabbarItem.Home,
TabbarItem.Profile
)
BottomNavigation(
// ...
) {
items.forEach { item ->
BottomNavigationItem(
// ...
selected = selectedTab == item,
// ...
Having a single navigation bar outside of NavHost, you can find an example in documentation