当前位置: 首页 > news >正文

Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel

在构建现代 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 应用。

记住:好的架构不是一开始就完美,而是在演进中保持清晰。

代码后续补充…

http://www.cnnetsun.cn/news/40592.html

相关文章:

  • CANN Samples(十九):特色场景:机器人 AI 绘画 手写识别等
  • 毕设开源 深度学习YOLO交通路面缺陷检测系统(源码+论文)
  • 【URP】Unity[后处理]色彩调整ColorAdjustments
  • Flutter混合开发与WebView集成实战
  • 压缩空气储能和释能阶段模型,附相关文档文献。 建立了压缩空气储能系统中的压缩机、换热器、储气罐...
  • Profinet转Modbus TCP工业数据采集网关:实现1200PLC 与打标卡数据实时传输
  • Flutter 2025 国际化与本地化实战:从多语言支持到文化适配,打造真正全球化的用户体验
  • P1178 到天宫做客 题解
  • 2025年GEO优化机会与争议以及规范发展的必要性
  • 2026老年春晚怀化区域节目征集启动仪式在怀化学院举行
  • 【笔记篇】【硬件基础篇】电力电子元器件应用手册 阅读笔记(1)电阻器及其应用
  • 柠檬 软件测试之python全栈自动化测试工程师第25期
  • 为什么你的视觉AI项目总是耗时又低效?Florence-2-large-ft一站式解决方案
  • 基于php的微信小程序的学习交流平台系统(源码+lw+部署文档+讲解等)
  • springboot高校读书分享系统的设计与实现_34ur7fyk-
  • 如何快速解锁RouterOS无限权限:MikroTikPatch的终极指南
  • 测试工程师的增值型职业休假策略:从技术深耕到跨界突破
  • USART串口
  • SDET职业生涯中的关键决策点与路径选择
  • 第三方API密集型聚合服务的测试体系构建
  • System Informer:你的Windows系统管家,3大核心功能深度解析
  • 探索城市脉搏:解密共享单车数据背后的故事
  • 如何用abogen构建高质量有声书生成系统:从单文件到批量处理的完整指南
  • FastText实战进阶:解锁文本处理的极致性能与多场景应用
  • 车载功能测试都要做什么?总结来了~
  • 终极指南:掌握CogVLM多模态大模型核心技术
  • 【毕业设计/课程设计】基于Python的热门微博数据可视化分析源码+论文+PPT+数据
  • 监控选购全攻略:6大场景首选品牌,海康威视/格行视精灵各有侧重,小米性价比,萤石更全能;格行视精灵AOV技术+终生免流真好用?
  • 5步掌握Three.js延迟渲染技术:从多光源卡顿到流畅渲染的终极指南
  • Profinet转ModbusTCP网关:实现西门子1200PLC与打标卡稳定通讯