Instantly share code, notes, and snippets.
Created
June 4, 2025 08:30
-
Star
2
(2)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save ardakazanci/fc3237771e654671561e8cf0bd1da2e0 to your computer and use it in GitHub Desktop.
BottomNavigation with Jetpack Navigation3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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