Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design Android app UI/UX following Material Design 3 guidelines with Jetpack Compose layout patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/android-navigation.md
1# Android Navigation Patterns23## Navigation Compose Basics45### Setup and Dependencies67```kotlin8// build.gradle.kts9dependencies {10implementation("androidx.navigation:navigation-compose:2.7.7")11// For type-safe navigation (recommended)12implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")13}14```1516### Basic Navigation1718```kotlin19@Serializable20object Home2122@Serializable23data class Detail(val itemId: String)2425@Serializable26object Settings2728@Composable29fun AppNavigation() {30val navController = rememberNavController()3132NavHost(33navController = navController,34startDestination = Home35) {36composable<Home> {37HomeScreen(38onItemClick = { itemId ->39navController.navigate(Detail(itemId))40},41onSettingsClick = {42navController.navigate(Settings)43}44)45}4647composable<Detail> { backStackEntry ->48val detail: Detail = backStackEntry.toRoute()49DetailScreen(50itemId = detail.itemId,51onBack = { navController.popBackStack() }52)53}5455composable<Settings> {56SettingsScreen(57onBack = { navController.popBackStack() }58)59}60}61}62```6364### Navigation with Arguments6566```kotlin67// Type-safe routes with arguments68@Serializable69data class ProductDetail(70val productId: String,71val category: String,72val fromSearch: Boolean = false73)7475@Serializable76data class UserProfile(77val userId: Long78)7980@Composable81fun NavigationWithArgs() {82val navController = rememberNavController()8384NavHost(navController = navController, startDestination = Home) {85composable<Home> {86HomeScreen(87onProductClick = { productId, category ->88navController.navigate(89ProductDetail(90productId = productId,91category = category,92fromSearch = false93)94)95}96)97}9899composable<ProductDetail> { backStackEntry ->100val args: ProductDetail = backStackEntry.toRoute()101ProductDetailScreen(102productId = args.productId,103category = args.category,104showBackToSearch = args.fromSearch105)106}107108composable<UserProfile> { backStackEntry ->109val args: UserProfile = backStackEntry.toRoute()110UserProfileScreen(userId = args.userId)111}112}113}114```115116## Bottom Navigation117118### Standard Implementation119120```kotlin121enum class BottomNavDestination(122val route: Any,123val icon: ImageVector,124val label: String125) {126HOME(Home, Icons.Default.Home, "Home"),127SEARCH(Search, Icons.Default.Search, "Search"),128FAVORITES(Favorites, Icons.Default.Favorite, "Favorites"),129PROFILE(Profile, Icons.Default.Person, "Profile")130}131132@Composable133fun MainScreenWithBottomNav() {134val navController = rememberNavController()135val navBackStackEntry by navController.currentBackStackEntryAsState()136val currentDestination = navBackStackEntry?.destination137138Scaffold(139bottomBar = {140NavigationBar {141BottomNavDestination.entries.forEach { destination ->142NavigationBarItem(143icon = {144Icon(destination.icon, contentDescription = destination.label)145},146label = { Text(destination.label) },147selected = currentDestination?.hasRoute(destination.route::class) == true,148onClick = {149navController.navigate(destination.route) {150// Pop up to start destination to avoid building up stack151popUpTo(navController.graph.findStartDestination().id) {152saveState = true153}154// Avoid multiple copies of same destination155launchSingleTop = true156// Restore state when reselecting157restoreState = true158}159}160)161}162}163}164) { innerPadding ->165NavHost(166navController = navController,167startDestination = Home,168modifier = Modifier.padding(innerPadding)169) {170composable<Home> { HomeScreen() }171composable<Search> { SearchScreen() }172composable<Favorites> { FavoritesScreen() }173composable<Profile> { ProfileScreen() }174}175}176}177```178179### Bottom Nav with Badges180181```kotlin182@Composable183fun BottomNavWithBadges(184cartCount: Int,185notificationCount: Int186) {187NavigationBar {188NavigationBarItem(189icon = { Icon(Icons.Default.Home, null) },190label = { Text("Home") },191selected = true,192onClick = { }193)194195NavigationBarItem(196icon = {197BadgedBox(198badge = {199if (cartCount > 0) {200Badge { Text("$cartCount") }201}202}203) {204Icon(Icons.Default.ShoppingCart, null)205}206},207label = { Text("Cart") },208selected = false,209onClick = { }210)211212NavigationBarItem(213icon = {214BadgedBox(215badge = {216if (notificationCount > 0) {217Badge {218Text(219if (notificationCount > 99) "99+"220else "$notificationCount"221)222}223}224}225) {226Icon(Icons.Default.Notifications, null)227}228},229label = { Text("Alerts") },230selected = false,231onClick = { }232)233}234}235```236237## Navigation Drawer238239### Modal Navigation Drawer240241```kotlin242@Composable243fun ModalDrawerNavigation() {244val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)245val scope = rememberCoroutineScope()246var selectedItem by remember { mutableStateOf(0) }247248val items = listOf(249DrawerItem(Icons.Default.Home, "Home"),250DrawerItem(Icons.Default.Settings, "Settings"),251DrawerItem(Icons.Default.Info, "About"),252DrawerItem(Icons.Default.Help, "Help")253)254255ModalNavigationDrawer(256drawerState = drawerState,257drawerContent = {258ModalDrawerSheet {259// Header260Box(261modifier = Modifier262.fillMaxWidth()263.height(180.dp)264.background(MaterialTheme.colorScheme.primaryContainer),265contentAlignment = Alignment.BottomStart266) {267Column(modifier = Modifier.padding(16.dp)) {268AsyncImage(269model = "avatar_url",270contentDescription = "Profile",271modifier = Modifier272.size(64.dp)273.clip(CircleShape)274)275Spacer(Modifier.height(8.dp))276Text(277"John Doe",278style = MaterialTheme.typography.titleMedium279)280Text(281"[email protected]",282style = MaterialTheme.typography.bodySmall283)284}285}286287Spacer(Modifier.height(12.dp))288289// Navigation items290items.forEachIndexed { index, item ->291NavigationDrawerItem(292icon = { Icon(item.icon, contentDescription = null) },293label = { Text(item.label) },294selected = index == selectedItem,295onClick = {296selectedItem = index297scope.launch { drawerState.close() }298},299modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)300)301}302303Spacer(Modifier.weight(1f))304305// Footer306HorizontalDivider()307NavigationDrawerItem(308icon = { Icon(Icons.Default.Logout, null) },309label = { Text("Sign Out") },310selected = false,311onClick = { },312modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)313)314Spacer(Modifier.height(12.dp))315}316}317) {318Scaffold(319topBar = {320TopAppBar(321title = { Text(items[selectedItem].label) },322navigationIcon = {323IconButton(onClick = { scope.launch { drawerState.open() } }) {324Icon(Icons.Default.Menu, "Open drawer")325}326}327)328}329) { padding ->330Content(modifier = Modifier.padding(padding))331}332}333}334335data class DrawerItem(val icon: ImageVector, val label: String)336```337338### Permanent Navigation Drawer (Tablets)339340```kotlin341@Composable342fun PermanentDrawerLayout() {343PermanentNavigationDrawer(344drawerContent = {345PermanentDrawerSheet(346modifier = Modifier.width(240.dp)347) {348Spacer(Modifier.height(12.dp))349Text(350"App Name",351modifier = Modifier.padding(16.dp),352style = MaterialTheme.typography.titleLarge353)354HorizontalDivider()355356drawerItems.forEach { item ->357NavigationDrawerItem(358icon = { Icon(item.icon, null) },359label = { Text(item.label) },360selected = item == selectedItem,361onClick = { selectedItem = item },362modifier = Modifier.padding(horizontal = 12.dp)363)364}365}366}367) {368// Main content takes remaining space369MainContent()370}371}372```373374## Navigation Rail375376```kotlin377@Composable378fun NavigationRailLayout() {379var selectedItem by remember { mutableStateOf(0) }380381Row(modifier = Modifier.fillMaxSize()) {382NavigationRail(383header = {384FloatingActionButton(385onClick = { },386elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()387) {388Icon(Icons.Default.Add, "Create")389}390}391) {392Spacer(Modifier.weight(1f))393394railItems.forEachIndexed { index, item ->395NavigationRailItem(396icon = { Icon(item.icon, null) },397label = { Text(item.label) },398selected = selectedItem == index,399onClick = { selectedItem = index }400)401}402403Spacer(Modifier.weight(1f))404}405406// Main content407Box(408modifier = Modifier409.weight(1f)410.fillMaxHeight()411) {412when (selectedItem) {4130 -> HomeContent()4141 -> SearchContent()4152 -> ProfileContent()416}417}418}419}420```421422## Deep Linking423424### Basic Deep Link Setup425426```kotlin427// In AndroidManifest.xml428// <intent-filter>429// <action android:name="android.intent.action.VIEW" />430// <category android:name="android.intent.category.DEFAULT" />431// <category android:name="android.intent.category.BROWSABLE" />432// <data android:scheme="myapp" />433// <data android:scheme="https" android:host="myapp.com" />434// </intent-filter>435436@Composable437fun DeepLinkNavigation() {438val navController = rememberNavController()439440NavHost(441navController = navController,442startDestination = Home443) {444composable<Home> {445HomeScreen()446}447448composable<ProductDetail>(449deepLinks = listOf(450navDeepLink<ProductDetail>(451basePath = "https://myapp.com/product"452),453navDeepLink<ProductDetail>(454basePath = "myapp://product"455)456)457) { backStackEntry ->458val args: ProductDetail = backStackEntry.toRoute()459ProductDetailScreen(productId = args.productId)460}461462composable<UserProfile>(463deepLinks = listOf(464navDeepLink<UserProfile>(465basePath = "https://myapp.com/user"466)467)468) { backStackEntry ->469val args: UserProfile = backStackEntry.toRoute()470UserProfileScreen(userId = args.userId)471}472}473}474```475476### Handling Intent in Activity477478```kotlin479class MainActivity : ComponentActivity() {480override fun onCreate(savedInstanceState: Bundle?) {481super.onCreate(savedInstanceState)482483setContent {484AppTheme {485val navController = rememberNavController()486487// Handle deep link from intent488LaunchedEffect(Unit) {489intent?.data?.let { uri ->490navController.handleDeepLink(intent)491}492}493494AppNavigation(navController = navController)495}496}497}498499override fun onNewIntent(intent: Intent) {500super.onNewIntent(intent)501// Handle new intents when activity is already running502setIntent(intent)503}504}505```506507## Nested Navigation508509```kotlin510@Composable511fun NestedNavigation() {512val navController = rememberNavController()513514NavHost(navController = navController, startDestination = MainGraph) {515// Main graph with bottom navigation516navigation<MainGraph>(startDestination = Home) {517composable<Home> {518HomeScreen(519onItemClick = { navController.navigate(Detail(it)) }520)521}522composable<Search> { SearchScreen() }523composable<Profile> {524ProfileScreen(525onSettingsClick = { navController.navigate(SettingsGraph) }526)527}528}529530// Nested detail graph531composable<Detail> { backStackEntry ->532val args: Detail = backStackEntry.toRoute()533DetailScreen(itemId = args.itemId)534}535536// Separate settings graph (full screen, no bottom nav)537navigation<SettingsGraph>(startDestination = SettingsMain) {538composable<SettingsMain> {539SettingsScreen(540onAccountClick = { navController.navigate(AccountSettings) },541onNotificationsClick = { navController.navigate(NotificationSettings) }542)543}544composable<AccountSettings> { AccountSettingsScreen() }545composable<NotificationSettings> { NotificationSettingsScreen() }546}547}548}549550@Serializable object MainGraph551@Serializable object SettingsGraph552@Serializable object SettingsMain553@Serializable object AccountSettings554@Serializable object NotificationSettings555```556557## Navigation State Management558559### ViewModel Integration560561```kotlin562@HiltViewModel563class NavigationViewModel @Inject constructor(564private val savedStateHandle: SavedStateHandle565) : ViewModel() {566567private val _navigationEvents = MutableSharedFlow<NavigationEvent>()568val navigationEvents = _navigationEvents.asSharedFlow()569570fun navigateToDetail(itemId: String) {571viewModelScope.launch {572_navigationEvents.emit(NavigationEvent.NavigateToDetail(itemId))573}574}575576fun navigateBack() {577viewModelScope.launch {578_navigationEvents.emit(NavigationEvent.NavigateBack)579}580}581}582583sealed class NavigationEvent {584data class NavigateToDetail(val itemId: String) : NavigationEvent()585object NavigateBack : NavigationEvent()586}587588@Composable589fun NavigationHandler(590navController: NavHostController,591viewModel: NavigationViewModel = hiltViewModel()592) {593LaunchedEffect(Unit) {594viewModel.navigationEvents.collect { event ->595when (event) {596is NavigationEvent.NavigateToDetail -> {597navController.navigate(Detail(event.itemId))598}599NavigationEvent.NavigateBack -> {600navController.popBackStack()601}602}603}604}605}606```607608### Back Handler609610```kotlin611@Composable612fun ScreenWithBackHandler(613onBack: () -> Unit614) {615var showExitDialog by remember { mutableStateOf(false) }616617// Intercept back press618BackHandler {619showExitDialog = true620}621622if (showExitDialog) {623AlertDialog(624onDismissRequest = { showExitDialog = false },625title = { Text("Exit App?") },626text = { Text("Are you sure you want to exit?") },627confirmButton = {628TextButton(onClick = onBack) {629Text("Exit")630}631},632dismissButton = {633TextButton(onClick = { showExitDialog = false }) {634Text("Cancel")635}636}637)638}639640// Screen content641Content()642}643```644645## Navigation Animations646647```kotlin648@Composable649fun AnimatedNavigation() {650val navController = rememberNavController()651652NavHost(653navController = navController,654startDestination = Home,655enterTransition = {656slideIntoContainer(657towards = AnimatedContentTransitionScope.SlideDirection.Left,658animationSpec = tween(300)659)660},661exitTransition = {662slideOutOfContainer(663towards = AnimatedContentTransitionScope.SlideDirection.Left,664animationSpec = tween(300)665)666},667popEnterTransition = {668slideIntoContainer(669towards = AnimatedContentTransitionScope.SlideDirection.Right,670animationSpec = tween(300)671)672},673popExitTransition = {674slideOutOfContainer(675towards = AnimatedContentTransitionScope.SlideDirection.Right,676animationSpec = tween(300)677)678}679) {680composable<Home> {681HomeScreen()682}683684composable<Detail>(685// Custom transition for specific route686enterTransition = {687fadeIn(animationSpec = tween(500)) +688scaleIn(initialScale = 0.9f, animationSpec = tween(500))689},690exitTransition = {691fadeOut(animationSpec = tween(500))692}693) {694DetailScreen()695}696}697}698```699