Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created June 4, 2025 08:30
Show Gist options
  • Save ardakazanci/fc3237771e654671561e8cf0bd1da2e0 to your computer and use it in GitHub Desktop.
Save ardakazanci/fc3237771e654671561e8cf0bd1da2e0 to your computer and use it in GitHub Desktop.
BottomNavigation with Jetpack Navigation3
@Serializable
sealed interface AppScreenKey : NavKey
@Serializable
sealed interface RootScreenKey : AppScreenKey {
@Serializable object Home : RootScreenKey
@Serializable object Profile : RootScreenKey
@Serializable object Settings : RootScreenKey
}
@Serializable
sealed interface SubScreenKey : AppScreenKey {
@Serializable object ProfileDetails : SubScreenKey
@Serializable object SettingsAbout : SubScreenKey
}
val AppScreenKeySaver = Saver<AppScreenKey, String>(
save = { Json.encodeToString(AppScreenKey.serializer(), it) },
restore = { Json.decodeFromString(AppScreenKey.serializer(), it) }
)
@Composable
fun AppWithBottomBar() {
val tabs = listOf(RootScreenKey.Home, RootScreenKey.Profile, RootScreenKey.Settings)
var selectedTab by rememberSaveable(stateSaver = AppScreenKeySaver) {
mutableStateOf(RootScreenKey.Home)
}
val tabBackStacks = remember {
TabBackStacks<AppScreenKey>(
startDestinations = tabs.associateWith { it }
)
}
val backStack = tabBackStacks.getStack(selectedTab)
Scaffold(
bottomBar = {
NavigationBar {
tabs.forEach { tab ->
val label = when (tab) {
RootScreenKey.Home -> "Home"
RootScreenKey.Profile -> "Profile"
RootScreenKey.Settings -> "Settings"
}
val icon = when (tab) {
RootScreenKey.Home -> Icons.Default.Home
RootScreenKey.Profile -> Icons.Default.Person
RootScreenKey.Settings -> Icons.Default.Settings
}
BottomBarItem(
label = label,
icon = icon,
selected = selectedTab == tab
) {
selectedTab = tab
}
}
}
}
) { padding ->
NavDisplay(
backStack = backStack,
modifier = Modifier.padding(padding),
onBack = { steps -> repeat(steps) { tabBackStacks.pop(selectedTab) } },
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
transitionSpec = {
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
},
entryProvider = entryProvider {
entry<RootScreenKey.Home> { HomeScreen() }
entry<RootScreenKey.Profile> {
ProfileScreen {
tabBackStacks.push(RootScreenKey.Profile, SubScreenKey.ProfileDetails)
}
}
entry<RootScreenKey.Settings> {
SettingsScreen {
tabBackStacks.push(RootScreenKey.Settings, SubScreenKey.SettingsAbout)
}
}
entry<SubScreenKey.ProfileDetails> { ProfileDetailScreen() }
entry<SubScreenKey.SettingsAbout> { SettingsAboutScreen() }
}
)
}
}
@Composable
fun HomeScreen(vm: HomeViewModel = viewModel()) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Home Count: ${vm.count}", style = MaterialTheme.typography.headlineMedium)
Button(onClick = { vm.increment() }) {
Text("Increase")
}
}
}
@Composable
fun ProfileScreen(onOpenDetail: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("👤 Profile")
Button(onClick = onOpenDetail) {
Text("Go to Detail")
}
}
}
@Composable
fun ProfileDetailScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("📄 Profile Detail", style = MaterialTheme.typography.headlineSmall)
}
}
@Composable
fun SettingsScreen(onOpenAbout: () -> Unit) {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("⚙️ Settings")
Button(onClick = onOpenAbout) {
Text("Go to About")
}
}
}
@Composable
fun SettingsAboutScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("ℹ️ Settings About", style = MaterialTheme.typography.headlineSmall)
}
}
class TabBackStacks<T : AppScreenKey>(val startDestinations: Map<T, T>) {
private val stacks = mutableStateMapOf<T, SnapshotStateList<T>>()
init {
startDestinations.forEach { (tab, root) ->
stacks[tab] = mutableStateListOf(root)
}
}
fun getStack(tab: T): SnapshotStateList<T> = stacks[tab]!!
fun push(tab: T, screen: T) {
stacks[tab]?.add(screen)
}
fun pop(tab: T) {
stacks[tab]?.removeLastOrNull()
}
fun current(tab: T): T = stacks[tab]?.last() ?: startDestinations[tab]!!
}
@Composable
fun RowScope.BottomBarItem(
label: String,
icon: ImageVector,
selected: Boolean,
onClick: () -> Unit
) {
NavigationBarItem(
selected = selected,
onClick = onClick,
icon = { Icon(icon, contentDescription = label) },
label = { Text(label) }
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment