在构建现代 Android 应用时,清晰的页面结构和合理的状态管理是保证项目可维护性和可扩展性的关键。Jetpack Compose 提供了声明式 UI 的强大能力,而Navigation+ViewModel的组合,则是实现复杂多页面应用的黄金搭档。
本文将通过一个完整、可运行、生产级风格的案例,带你一步步实现:
- 启动页(Splash)
- 登录页(Login)
- 主页(带底部导航:首页 / 通讯录 / 我的)
- 每个 Tab 拥有自己独立的 ViewModel
- 全局登录状态统一管理
并深入探讨页面拆分、导航设计、状态隔离等核心工程实践。
🎯 最终效果预览
所有页面逻辑分离,职责清晰,切换 Tab 不会丢失状态!
📁 推荐项目结构
良好的目录结构是大型项目的基石:
com.example.myapp/├── MyApp.kt// App 根组件├── navigation/NavGraph.kt //全局导航图 ├── viewmodel/AuthViewModel.kt //全局登录状态 ├── route/ModuleRoute.kt //路由常量 └── ui/├── splash/SplashScreen.kt ├── login/LoginScreen.kt └── main/├── MainScreen.kt// 底部导航容器├── home/HomeTab.kt+HomeViewModel.kt ├── contacts/ContactsTab.kt+ContactsViewModel.kt └── profile/ProfileTab.kt+ProfileViewModel.kt🔑 核心依赖
implementation("androidx.navigation:navigation-compose:2.8.0")implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0")1️⃣ 全局状态:AuthViewModel
用于管理用户是否已登录,供整个 App 使用:
classAuthViewModel:ViewModel(){privateval_flowLogin=MutableStateFlow(false)valflowLogin=_flowLogin.asStateFlow()// 模拟登录成功funlogin(){_flowLogin.value=true}funloginOut(){_flowLogin.value=false}}在 MyApp.kt 中通过 viewModel() 获取单例实例。
2️⃣ 页面专属 ViewModel:解耦业务逻辑
通讯录 ViewModel
dataclassContact(valid:Int,valname:String,valphone:String)classContactsViewModel:ViewModel(){privateval_contacts=MutableStateFlow<MutableList<Contact>>(mutableListOf())valcontacts:StateFlow<List<Contact>>=_contacts.asStateFlow()privatevar_isLoading=MutableStateFlow(true)valisLoading=_isLoading.asStateFlow()init{loadContacts()}privatefunloadContacts(){viewModelScope.launch{_isLoading.value=truedelay(1000)// 模拟网络请求_contacts.value=mutableListOf(Contact(1,"张三","138****1234"),Contact(2,"李四","139****5678"))_isLoading.value=false}}funrefresh(){loadContacts()}}💡 优势:
- 数据加载、错误处理、刷新逻辑全部封装在 ViewModel
- UI 层只负责展示,完全无业务逻辑
- 切换 Tab 时,ViewModel 实例由 Navigation 自动保存(只要 route 不变)
3️⃣ 页面 UI:自动注入 ViewModel
在 Composable 中直接使用viewModel()获取专属实例:
@ComposablefunContactsTab(contactsViewModel:ContactsViewModel=viewModel(),modifier:Modifier=Modifier,){valcontactsbycontactsViewModel.contacts.collectAsStateWithLifecycle()valisLoadingbycontactsViewModel.isLoading.collectAsStateWithLifecycle()Box(modifier=modifier.fillMaxSize(),contentAlignment=Alignment.TopStart){Text("📇 通讯录",fontSize=24.sp)}if(isLoading){Box(modifier=Modifier.fillMaxSize(),contentAlignment=Alignment.Center){CircularProgressIndicator()}}else{LazyColumn(modifier=Modifier.fillMaxSize().padding(top=56.dp),contentPadding=PaddingValues(16.dp)){items(contacts.size){contact->Card(modifier=Modifier.fillMaxWidth().padding(vertical=4.dp),elevation=CardDefaults.cardElevation(defaultElevation=2.dp)){Row(modifier=Modifier.padding(16.dp),verticalAlignment=Alignment.CenterVertically){Column{Text(contacts[contact].name,style=MaterialTheme.typography.titleMedium)Text(contacts[contact].phone,style=MaterialTheme.typography.bodySmall)}}}}}}}✅ collectAsStateWithLifecycle() 会自动在 Composable 进入后台时暂停收集,避免内存泄漏。
4️⃣ 导航设计:嵌套路由 + 状态清理
全局导航图(NavGraph.kt)
@ComposablefunNavGraph(navigationControl:NavHostController,authViewModel:AuthViewModel){NavHost(navController=navigationControl,startDestination=ModuleRoute.Splash){composable<ModuleRoute.Splash>{SplashScreen(onTimeOut={if(authViewModel.flowLogin.value){navigationControl.navigate(ModuleRoute.Main){popUpTo(ModuleRoute.Splash){inclusive=true}}}else{navigationControl.navigate(ModuleRoute.Login){popUpTo(ModuleRoute.Splash){inclusive=true}}}})}composable<ModuleRoute.Login>{LoginScreen(onLoginClick={authViewModel.login()navigationControl.navigate(ModuleRoute.Main){popUpTo(ModuleRoute.Login){inclusive=true}}})}composable<ModuleRoute.Main>{MainScreen(onLogout={authViewModel.loginOut()navigationControl.navigate(ModuleRoute.Login){popUpTo(ModuleRoute.Main){inclusive=true}}})}}}主页内部:嵌套 NavHost 实现底部导航
@ComposablefunMainScreen(onLogout:()->Unit,modifier:Modifier=Modifier,navController:NavHostController=rememberNavController(),){// 定义底部tabvalitems=listOf(BottomNavItem.Home,BottomNavItem.Contacts,BottomNavItem.Profile)BackHandler(enabled=true){Log.e("test","进入BackHandler")}Scaffold(bottomBar={NavigationBar{valnavBackStackEntrybynavController.currentBackStackEntryAsState()valcurrentRoute=navBackStackEntry?.destination?.route items.forEach{item->NavigationBarItem(icon={Icon(item.icon,contentDescription=null)},label={Text(item.title)},selected=currentRoute==item.route,onClick={navController.navigate(item.route){// 避免重复入栈popUpTo(navController.graph.id){saveState=trueinclusive=false}launchSingleTop=truerestoreState=true}})}}},){innerPadding->NavHost(navController=navController,startDestination=BottomNavItem.Home.route,modifier=modifier.padding(innerPadding)){composable(BottomNavItem.Home.route){HomeTab()}composable(BottomNavItem.Contacts.route){ContactsTab()}composable(BottomNavItem.Profile.route){ProfileTab(onLogout=onLogout)}}}}// 定义底部导航项sealedclassBottomNavItem(valroute:String,valtitle:String,valicon:ImageVector){dataobjectHome:BottomNavItem("home","首页",Icons.Default.Home)dataobjectContacts:BottomNavItem("contacts","通讯录",Icons.Default.Person)dataobjectProfile:BottomNavItem("profile","我的",Icons.Default.AccountCircle)}🌟 为什么用嵌套路由?
官方推荐做法!避免底部 Tab 切换时重建整个页面,同时支持每个 Tab 内部继续跳转子页面(如联系人详情)。
5️⃣ 关键技巧总结
| 场景 | 解决方案 |
|---|---|
| 页面太多? | 拆分到不同文件,按功能模块组织目录 |
| 状态混乱? | 全局状态用共享 ViewModel,局部状态用页面专属 ViewModel |
| 返回栈错乱? | 使用 popUpTo(…) + inclusive = true 清理历史 |
| Tab 切换重建? | 确保使用嵌套路由,Navigation 会自动保存状态 |
| 内存泄漏? | 用 collectAsStateWithLifecycle() 替代 collectAsState() |
✅ 为什么这样做是“最佳实践”?
- 高内聚低耦合:每个页面只关心自己的数据和 UI
- 易于测试:ViewModel 可单元测试,UI 可 Preview 预览
- 团队协作友好:多人开发不同 Tab 互不干扰
- 可扩展性强:未来添加“消息”Tab 只需复制模式
- 符合官方架构指南:遵循 Guide to app architecture
📚 结语
Jetpack Compose 不只是“新 UI 框架”,更是一种全新的应用架构思维。通过合理拆分页面、隔离状态、规范导航,我们可以构建出既简洁又强大的现代化 Android 应用。
记住:好的架构不是一开始就完美,而是在演进中保持清晰。
代码后续补充…