跳到正文 Lab Week 6 Summary · Jimmy Huang's Blog
Goal 在 Compose 项目里加入导航依赖
建立多个 screen,并为每个 screen 定义 route
用 NavController、 NavHost、 NavGraph 完成导航
实作 expanded modal navigation rail
实作 bottom navigation bar
理解 Scaffold、 TopAppBar、 NavigationBar / NavigationRail 各自负责什么
用 ViewModel 做跨 screen 状态共享
用 LazyColumn 显示一组数据
下面所有 Kotlin 文件都建议放在和 MainActivity.kt 同一个 package 下。
Task 0: 创建项目并补齐依赖 新建一个 Compose 项目 NavigationApp
把 Compose Navigation 依赖加进去
确保 Gradle sync 成功
这一步做对,后面的 rememberNavController()、 NavHost、 composable() 才会识别
Step 0.1:创建新项目 在 Android Studio 里新建一个 Compose 项目,项目名填 NavigationApp。
先建立一个干净的导航实验项目,避免和旧代码互相影响。
项目能正常打开, MainActivity.kt 能看到默认的 setContent { ... }。
Step 0.2:在模块级 build.gradle.kts 加入导航依赖 把下面依赖加到 app 模块的 build.gradle.kts 里:
把 Compose Navigation 这套功能接进项目里。
这周不管是 rail 还是 bottom bar,底层都依赖同一套 navigation API。
可以点黄灯,把依赖改成 library catalog declaration。改完之后通常会变成:
如果你项目真的在用 libs.versions.toml,也可以手动补:
Step 0.3:Sync Project 点 Android Studio 上方的 Sync Now ,等待 Gradle 同步完成。
Task 1: 创建 3 个基础 Screen
Step 1.1:创建 Home.kt先做一个最简单的可显示页面,后面只要 route 切换正确,就能立刻看出有没有跳转成功。
Step 1.2:创建 History.kt 和 Profile.kt把另外两个 screen 也补齐,后面才能做三页面导航。
Step 1.3:先单独验证一个 Screen 能不能显示 你可以先在 MainActivity.kt 暂时这样写,确认 Compose 页面本身没问题:
如果连 HomeScreen() 都显示不出来,后面做导航时会更难排查。
运行后,模拟器中央能看到 Home Screen 文字。
Task 2: 创建 routes / destinations 用一个统一的地方管理 route、label、icon
这一步会把 screen 和导航菜单对应起来
后面 rail 和 bottom bar 都直接遍历这个 destination 列表
route 字串必须和 NavHost 里的 composable(route) 对得上
Step 2.1:创建 Destination.kt后面做导航菜单时,不需要手写三遍 NavigationRailItem / NavigationBarItem,直接循环 Destination.entries 就可以。
Step 2.2:确认项目结构清楚 先把文件放整齐,后面 rail / bottom bar / advanced app 不容易混掉。
Android Studio 左侧项目树能清楚看到这些文件。
Task 3: 实作 Expanded Modal Navigation Rail
Step 3.1:准备 rail 顶部关闭图标 官方做法是把 Moodle 提供的 menu_open_24dp.png 放到:
Lab 示例不是直接用内建图标,而是用了一个 drawable 资源。
drawable 文件名只能用小写字母、数字、底线
如果你的文件名不是 menu_open_24dp,代码里的 R.drawable.xxx 也要一起改
你暂时没有这张图时,可以先把对应 Icon(...) 改成 Icons.Default.Close,先验证功能,再补官方图标
Step 3.2:创建 ExpandedModalNavigationRail() 组合函数 新建一个文件,例如 NavigationUI.kt,写入下面这段代码:
把 rail UI、top bar、drawer 状态、导航逻辑全部接到一起。
这才是这周 Compose Navigation 的核心骨架。
前面做的 3 个 screen 和 Destination enum,到这里才真正连起来。
rememberNavController()负责创建导航控制器,管理页面切换和 back stack
currentBackStackEntryAsState()让我们知道“当前在哪个 route”,这样才能让当前菜单项高亮
ModalNavigationDrawer + rememberDrawerState()让 rail 变成可开关的 modal drawer
rememberCoroutineScope()drawerState.open() / drawerState.close() 是挂起操作,所以要在 coroutine 里执行
NavHost(...) { composable(...) }把 route 和实际页面绑定起来
Modifier.padding(innerPadding)这是 Scaffold 给内容区的安全边距,不加的话内容可能压到 top bar 下面
Step 3.3:在 MainActivity.kt 调用它 把 App 的入口页面切换成 rail 导航版本。
如果你的主题函数不是 NavigationAppTheme,改成 Android Studio 自动生成的主题名。
Task 4: 实作 Bottom Navigation Bar 这一部分复用前面已经建好的 3 个 screen 和 Destination
只是把导航入口从侧边 rail 改到底部 bar
底层还是同一套 NavController + NavHost
Bottom Navigation 一般适合 3 到 5 个顶级入口
Step 4.1:创建 BottomNavigationBar() 组合函数 还是放在 NavigationUI.kt 里,新增下面这段代码:
把相同的 3 个 destination,改用底部导航栏来呈现。
Navigation Rail 和 Bottom Navigation Bar 只是“菜单长什么样”不同,真正切换页面的还是同一套 navigation 机制。
bottomBar = { ... }是 Scaffold 专门留给底部导航区的插槽
NavigationBarItem(...)每一个 item 对应一个 destination
selected = currentDestination?.route == destination.route让当前页面对应的按钮高亮
onClick { navController.navigate(...) }真正触发页面切换
Step 4.2:切换 MainActivity.kt 的入口 把 App 入口从 rail 版本切换成 bottom bar 版本。
Step 4.3:运行并验证 Bottom Navigation
Task 5: Problem Solving - 完成 Trip Diary App 这一部分建议单独新建一个项目,避免和前面的 demo 代码互相覆盖
要求是: Home 展示标题和图片, Trip 收集资料, Diary 显示资料
这里最关键的是: 表单数据要能跨 screen 共用
最稳妥的做法就是: 一个 Activity 级别的 ViewModel + 一个 Bottom Navigation App
下面给你一版可以直接照做、也比较容易讲清楚原理的实现路线
Step 5.1:新建第二个项目,并准备图片资源 把进阶题目和前面 demo 分开,比较不容易把代码改乱。
Step 5.2:建立资料模型和共享 ViewModel 把 Trip 页填进来的资料统一存进一个共享状态容器里。
Trip 和 Diary 是两个不同 screen。
如果不用共享状态, Trip 填完的数据一切屏就很容易拿不到。
前 4 个 mutableStateOf(...)存放当前正在输入的表单内容
_tripList存放已经提交成功的 trip 记录
saveTrip()负责做最基本的“是否填完整”检查,并把一笔 trip 加进列表
Lab 原文说最基础版只支持一笔 trip,但也明确提到更好的做法是支持多笔,并在 Diary 用 LazyColumn 显示。这里直接做成多笔版本,后面展示和回答老师问题都会更完整。
Step 5.3:建立新的 destinations 把 Home / Trip / Diary 三个功能页注册成导航入口。
后面的 bottom bar 和 NavHost 都要靠这份 destination 清单来工作。
Step 5.4:创建 TripHomeScreen.kt完成题目要求里的 Home Screen will show an app title and its relevant image in the middle of the screen。
Step 5.5:创建 TripScreen.ktFull Name 输入框
Email 输入框
DatePicker
国家下拉菜单
提交按钮
用户能填数据
数据会进入共享 ViewModel
提交后 Diary 能读到这些数据
rememberDatePickerState()管理 DatePicker 当前选中的日期
showDatePicker控制日期选择对话框开关
countryMenuExpanded控制国家下拉菜单开关
viewModel::updateFullName / updateEmail让输入内容直接写进共享状态
viewModel.saveTrip()真正把一笔 trip 存进列表
能正常输入姓名和邮箱
点日期图标后能弹出 DatePicker
选国家时能从下拉菜单里选值
点 Submit 后会跳去 Diary
Step 5.6:创建 DiaryScreen.kt把已经提交的 trip 记录显示出来,而且是用 LazyColumn 做列表显示。
Step 5.7:创建整个 App 的导航骨架 把 Home、 Trip、 Diary 三页和底部导航真正接起来,并且保证三页共用同一个 UserDataViewModel。
如果你每个 screen 自己 new 一个 ViewModel, Trip 填进去的数据不会真正流到 Diary。
private val userDataViewModel by viewModels()在 Activity 层创建一个共享 ViewModel 实例
TripDiaryApp(viewModel = userDataViewModel)把同一个实例传给导航系统里的 screen
onSubmitSuccess提交成功后自动切去 Diary
Step 5.8:运行并完整验证 打开 App,确认 Home 页有标题和图片
切到 Trip
输入姓名、邮箱
选择日期
选择国家
点 Submit
自动跳到 Diary
确认 Diary 列表里看得到刚才的数据
再新增第二笔,确认 LazyColumn 能显示多笔资料
Summary 这周表面上是在做两种导航组件,实际上真正学到的是这条主线:
页面本身 是普通的 Composable
route 是页面的内部地址
NavController 负责切换和维护 back stack
NavHost 负责把 route 和 screen 连起来
Navigation Rail / Bottom Navigation Bar 只是导航入口的不同外观
Scaffold 负责把 top bar、bottom bar、内容区排好
ViewModel 负责让不同 screen 共享状态
LazyColumn 负责把多笔数据展示成列表
也就是说,这周不是“学两个组件”而已,而是开始真正进入 Compose 多页面应用的基本架构 。
本周必须理解的知识点
1)Compose Navigation 的 3 个核心角色 NavController:导航的大脑,负责 navigate()、维护 back stack
NavHost:导航的显示容器,把 controller 和页面区域连起来
NavGraph:你定义的页面关系表,通常就是 NavHost { composable(...) } 这一段
2)为什么 route 要单独定义 因为 screen 的切换不是靠手点某个 composable,而是靠一个字符串 route 去定位。
navigate("home")、 navigate("profile") 这些 route 一旦乱掉,导航就会直接失效。
3)为什么 Scaffold 不能省 因为 top bar、bottom bar 都是有布局边界的。
Scaffold 会给内容区一个 paddingValues / innerPadding,你必须把它传给 NavHost 或内容区,否则内容很容易被 bar 挡住。
4)为什么 modal rail 需要 coroutine 因为 drawer 的开关是状态切换,而且相关 API 是挂起操作。
5)为什么 Trip / Diary 要用 ViewModel 如果不把状态提到共享层,页面一切换,资料就会断掉。
关键概念对比
Navigation Rail vs Bottom Navigation Bar Navigation Rail在侧边
更适合宽一点的布局,或者你想让导航更明显的时候
本周做的是 expanded modal 版本
Bottom Navigation Bar在底部
很适合手机布局
一般放 3 到 5 个顶级入口最合适
NavController vs NavHost vs NavGraph
popUpTo vs launchSingleTop vs restoreState
route vs screen
常见报错与排查
1) rememberNavController / NavHost / composable 全都报红 navigation-compose 有没有真的加到模块级 build.gradle.kts
有没有点 Sync Now
代码里有没有正确 import
2) by navController.currentBackStackEntryAsState() 报 getValue 相关错误
3) R.drawable.menu_open_24dp 报错 图片有没有真的放在 res/drawable/
文件名是不是全小写加底线
代码里的资源名和文件名是不是一致
4) Icons.Default.xxx 报 unresolved reference
5)页面能跑,但点导航没反应 Destination 的 route 是否和 NavHost 里的 composable(route) 完全一致
onClick 里有没有真的写 navController.navigate(destination.route)
MainActivity 当前到底调用的是哪一个入口 composable
6) Trip 填完了, Diary 还是空的 你没有共用同一个 ViewModel
你在不同 screen 各自创建了自己的状态
saveTrip() 根本没执行成功
MainActivity 是否用 by viewModels() 建了一个共享 ViewModel
TripDiaryApp(viewModel = userDataViewModel) 是否传了同一个实例
saveTrip() 是否真的返回 true
7)DatePicker 没弹出来 showDatePicker 有没有真的改成 true
TripScreen 有没有加:
触发按钮的 onClick 有没有写错
老师可能会问什么
1)为什么 rail 和 bottom bar 都能导航,但代码长得不一样? 真正负责切换页面的还是同一套 NavController + NavHost + route。
2)为什么需要 currentBackStackEntryAsState()? 因为你要知道“当前在哪个 destination”,才能让当前的 item 高亮,否则菜单不会跟页面状态同步。
3)为什么要写 popUpTo + launchSingleTop + restoreState? 为了避免重复压栈、切 tab 时状态乱掉、页面越切越多。
4)为什么 Scaffold 的 paddingValues 要传给 NavHost? 因为 top bar / bottom bar 会占位置。
不传 padding,内容区可能被 bar 压住。
5)为什么 Trip 和 Diary 之间适合用 ViewModel? 因为它们是两个不同 screen,但共享同一份业务数据。
把状态放进 ViewModel,可以让数据跨页面存在,也比较符合 Compose / Android 的常见写法。
6)为什么 Diary 要用 LazyColumn,不用普通 Column? 列表类数据用 LazyColumn 更合适,也更接近真实 app 的写法。
7)Navigation Rail 和 Bottom Navigation 该怎么选?
[versions]
navigationCompose = "2.9.3"
[libraries]
androidx-navigation-compose = {group = "androidx.navigation",name = "navigation-compose",version.ref = "navigationCompose" }java/com.yourpackage.navigationapp/
├── MainActivity.kt
├── Home.kt
├── History.kt
├── Profile.kt
├── Destination.kt
└── NavigationUI.ktapp/src/main/res/drawable/app/src/main/res/drawable/trip_image.pngjava/com.yourpackage.tripdiaryapp/
├── MainActivity.kt
├── TripDestination.kt
├── UserDataViewModel.kt
├── TripHomeScreen.kt
├── TripScreen.kt
├── DiaryScreen.kt
└── TripDiaryApp.kt
res/drawable/
└── trip_image.pngdependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation( "androidx.navigation:navigation-compose:2.9.3" ) // 建议一起补上,避免 Icons.Default.* 报 unresolved reference implementation( "androidx.compose.material:material-icons-extended" ) } implementation(libs.androidx.navigation.compose) @Composable fun HomeScreen() { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "Home Screen" , style = MaterialTheme.typography.headlineMedium ) } } @Composable fun HistoryScreen() { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "History Screen" , style = MaterialTheme.typography.headlineMedium ) } } @Composable fun ProfileScreen() { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "Profile Screen" , style = MaterialTheme.typography.headlineMedium ) } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { NavigationAppTheme { HomeScreen() } } } } enum class Destination( val route: String, val label: String, val icon: ImageVector ) { HOME( "home" , "Home" , Icons.Default.Home), HISTORY( "history" , "History" , Icons.Default.Info), PROFILE( "profile" , "Profile" , Icons.Default.AccountCircle) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ExpandedModalNavigationRail() { val navController = rememberNavController() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination ModalNavigationDrawer( drawerState = drawerState, drawerContent = { NavigationRail( containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) { Spacer(modifier = Modifier.height(16.dp)) NavigationRailItem( selected = false, onClick = { scope.launch { drawerState.close() } }, icon = { Icon( painter = painterResource(id = R.drawable.menu_open_24dp), contentDescription = "Close drawer" , modifier = Modifier.size(26.dp) ) } ) Spacer(modifier = Modifier.height(16.dp)) Destination.entries.forEach { destination -> NavigationRailItem( selected = currentDestination?.route == destination.route, onClick = { navController.navigate(destination.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } scope.launch { drawerState.close() } }, icon = { Icon( imageVector = destination.icon, contentDescription = destination.label ) }, label = { Text(destination.label) } ) Spacer(modifier = Modifier.height(16.dp)) } } } ) { Scaffold( topBar = { TopAppBar( title = { Text( text = "Navigation Rail Demo App" , style = MaterialTheme.typography.titleMedium ) }, navigationIcon = { IconButton( onClick = { scope.launch { if (drawerState.isClosed) { drawerState.open() } else { drawerState.close() } } } ) { Icon(Icons.Default.Menu, contentDescription = "Menu" ) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color(0xFFB39DDB), titleContentColor = Color.White, navigationIconContentColor = Color.White ) ) } ) { innerPadding -> NavHost( navController = navController, startDestination = Destination.HOME.route, modifier = Modifier.padding(innerPadding) ) { composable(Destination.HOME.route) { HomeScreen() } composable(Destination.HISTORY.route) { HistoryScreen() } composable(Destination.PROFILE.route) { ProfileScreen() } } } } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { NavigationAppTheme { ExpandedModalNavigationRail() } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun BottomNavigationBar() { val navController = rememberNavController() Scaffold( topBar = { TopAppBar( title = { Text( text = "Navigation Bar Demo App" , style = MaterialTheme.typography.titleMedium ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color(0xFFB39DDB), titleContentColor = Color.White, navigationIconContentColor = Color.White ) ) }, bottomBar = { NavigationBar( modifier = Modifier.padding(bottom = 20.dp) ) { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Destination.entries.forEach { destination -> NavigationBarItem( icon = { Icon( imageVector = destination.icon, contentDescription = destination.label ) }, label = { Text(destination.label) }, selected = currentDestination?.route == destination.route, onClick = { navController.navigate(destination.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } ) { paddingValues -> NavHost( navController = navController, startDestination = Destination.HOME.route, modifier = Modifier.padding(paddingValues) ) { composable(Destination.HOME.route) { HomeScreen() } composable(Destination.HISTORY.route) { HistoryScreen() } composable(Destination.PROFILE.route) { ProfileScreen() } } } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { NavigationAppTheme { BottomNavigationBar() // ExpandedModalNavigationRail() } } } } data class TripEntry( val fullName: String, val email: String, val tripDate: String, val country: String ) class UserDataViewModel : ViewModel() { var fullName by mutableStateOf( "" ) private set var email by mutableStateOf( "" ) private set var tripDate by mutableStateOf( "" ) private set var country by mutableStateOf( "" ) private set private val _tripList = mutableStateListOf<TripEntry>() val tripList: List<TripEntry> = _tripList fun updateFullName( value : String) { fullName = value } fun updateEmail( value : String) { email = value } fun updateTripDate( value : String) { tripDate = value } fun updateCountry( value : String) { country = value } fun saveTrip(): Boolean { if ( fullName.isBlank() || email.isBlank() || tripDate.isBlank() || country.isBlank() ) { return false } _tripList.add( TripEntry( fullName = fullName.trim(), email = email.trim(), tripDate = tripDate, country = country ) ) fullName = "" email = "" tripDate = "" country = "" return true } } enum class TripDestination( val route: String, val label: String, val icon: ImageVector ) { HOME( "home" , "Home" , Icons.Default.Home), TRIP( "trip" , "Trip" , Icons.Default.Edit), DIARY( "diary" , "Diary" , Icons.Default.List) } @Composable fun TripHomeScreen() { Column( modifier = Modifier .fillMaxSize() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Welcome to My Travel Diary" , style = MaterialTheme.typography.headlineSmall ) Spacer(modifier = Modifier.height(24.dp)) Image( painter = painterResource(id = R.drawable.trip_image), contentDescription = "Trip image" , modifier = Modifier .fillMaxWidth() .height(220.dp), contentScale = ContentScale.Crop ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun TripScreen( viewModel: UserDataViewModel, onSubmitSuccess: () -> Unit ) { var showDatePicker by remember { mutableStateOf(false) } var countryMenuExpanded by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf<String?>(null) } val countries = listOf( "Australia" , "Italy" , "Japan" , "Singapore" , "New Zealand" ) val datePickerState = rememberDatePickerState() if (showDatePicker) { DatePickerDialog( onDismissRequest = { showDatePicker = false }, confirmButton = { TextButton( onClick = { datePickerState.selectedDateMillis?.let { millis -> val formatter = SimpleDateFormat( "yyyy-MM-dd" , Locale.getDefault() ) viewModel.updateTripDate(formatter.format(Date(millis))) } showDatePicker = false } ) { Text( "OK" ) } }, dismissButton = { TextButton( onClick = { showDatePicker = false } ) { Text( "Cancel" ) } } ) { DatePicker(state = datePickerState) } } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text( text = "Profile Details" , style = MaterialTheme.typography.headlineSmall ) OutlinedTextField( value = viewModel.fullName, onValueChange = viewModel::updateFullName, label = { Text( "Full Name" ) }, modifier = Modifier.fillMaxWidth(), singleLine = true ) OutlinedTextField( value = viewModel.email, onValueChange = viewModel::updateEmail, label = { Text( "Email" ) }, modifier = Modifier.fillMaxWidth(), singleLine = true ) OutlinedTextField( value = viewModel.tripDate, onValueChange = {}, label = { Text( "Trip Date" ) }, readOnly = true, modifier = Modifier.fillMaxWidth(), trailingIcon = { IconButton( onClick = { showDatePicker = true } ) { Icon( imageVector = Icons.Default.DateRange, contentDescription = "Choose date" ) } } ) Box { OutlinedTextField( value = viewModel.country, onValueChange = {}, label = { Text( "Country" ) }, readOnly = true, modifier = Modifier.fillMaxWidth(), trailingIcon = { IconButton( onClick = { countryMenuExpanded = true } ) { Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "Choose country" ) } } ) DropdownMenu( expanded = countryMenuExpanded, onDismissRequest = { countryMenuExpanded = false } ) { countries.forEach { country -> DropdownMenuItem( text = { Text(country) }, onClick = { viewModel.updateCountry(country) countryMenuExpanded = false } ) } } } Button( onClick = { val saved = viewModel.saveTrip() if (saved) { errorMessage = null onSubmitSuccess() } else { errorMessage = "请先把姓名、邮箱、日期和国家都填完整。" } }, modifier = Modifier.fillMaxWidth() ) { Text( "Submit" ) } errorMessage?.let { Text( text = it, color = MaterialTheme.colorScheme.error ) } } } @Composable fun DiaryScreen(viewModel: UserDataViewModel) { val trips = viewModel.tripList if (trips.isEmpty()) { Box( modifier = Modifier .fillMaxSize() .padding(24.dp), contentAlignment = Alignment.Center ) { Text( "还没有 Trip 资料,请先到 Trip 页面新增一笔。" ) } return } LazyColumn( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { item { Text( text = "Trip Diary" , style = MaterialTheme.typography.headlineSmall ) } items(trips) { trip -> Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp) ) { Text( "Full Name: ${ trip.fullName } " ) Text( "Email: ${ trip.email } " ) Text( "Trip Date: ${ trip.tripDate } " ) Text( "Country: ${ trip.country } " ) } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun TripDiaryApp(viewModel: UserDataViewModel) { val navController = rememberNavController() Scaffold( topBar = { TopAppBar( title = { Text( "Trip Diary App" ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color(0xFFB39DDB), titleContentColor = Color.White ) ) }, bottomBar = { NavigationBar { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination TripDestination.entries.forEach { destination -> NavigationBarItem( icon = { Icon( imageVector = destination.icon, contentDescription = destination.label ) }, label = { Text(destination.label) }, selected = currentDestination?.route == destination.route, onClick = { navController.navigate(destination.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } ) { paddingValues -> NavHost( navController = navController, startDestination = TripDestination.HOME.route, modifier = Modifier.padding(paddingValues) ) { composable(TripDestination.HOME.route) { TripHomeScreen() } composable(TripDestination.TRIP.route) { TripScreen( viewModel = viewModel, onSubmitSuccess = { navController.navigate(TripDestination.DIARY.route) { launchSingleTop = true } } ) } composable(TripDestination.DIARY.route) { DiaryScreen(viewModel = viewModel) } } } } class MainActivity : ComponentActivity() { private val userDataViewModel: UserDataViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { TripDiaryAppTheme { TripDiaryApp(viewModel = userDataViewModel) } } } } val scope = rememberCoroutineScope() scope.launch { drawerState.open() } import androidx.compose.runtime.getValue implementation( "androidx.compose.material:material-icons-extended" ) @OptIn(ExperimentalMaterial3Api::class)