☰
【Jetpack Compose进阶学与练】第4课:ViewModel结合Navigation导航;不同页面之间共享ViewModel
2026/10/9 2:38:22 网站建设 项目流程

前言

上一课学习了基础ViewModel;默认情况下,每一个导航页面(composable目的地)各自拥有独立的ViewModel实例。
如果希望多个导航页面之间共享同一份ViewModel(例如多页表单,A页录入、B页继续编辑),就不能直接在页面内部调用viewModel()。
本节课学习Nav‑BackStackEntry的ViewModelStore;实现导航作用域下共享ViewModel。

知识点清单

  1. 默认行为:每个composable页面各自隔离ViewModel实例
  2. NavGraph导航图作用域共享ViewModel(整个导航范围之内单例)
  3. 获取导航回退栈entry拿到ViewModelStoreOwner;基于该owner获取共享ViewModel
  4. 多页面表单示例:录入页A,预览页B,共享一份表单ViewModel
  5. 共享ViewModel的销毁时机:整个导航回退栈全部弹出之后才销毁

完整示例源码

importandroidx.lifecycle.ViewModelimportandroidx.lifecycle.viewModelScopeimportkotlinx.coroutines.flow.MutableStateFlowimportkotlinx.coroutines.flow.StateFlowimportkotlinx.coroutines.launchimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.collectAsStateWithLifecycleimportandroidx.compose.runtime.getValueimportandroidx.navigation.NavHostControllerimportandroidx.navigation.compose.NavHostimportandroidx.navigation.compose.composableimportandroidx.navigation.compose.rememberNavControllerimportandroidx.lifecycle.viewmodel.compose.viewModelobjectRoute{constvalFORM_INPUT="form_input"constvalFORM_PREVIEW="form_preview"}classFormShareViewModel:ViewModel(){privateval_nameFlow=MutableStateFlow("")valnameFlow:StateFlow<String>=_nameFlowprivateval_phoneFlow=MutableStateFlow("")valphoneFlow:StateFlow<String>=_phoneFlowfunupdateName(text:String){viewModelScope.launch{_nameFlow.value=text}}funupdatePhone(text:String){viewModelScope.launch{_phoneFlow.value=text}}}@ComposablefunShareVmNavPage(){valnavController=rememberNavController()NavHost(navController=navController,startDestination=Route.FORM_INPUT){composable(Route.FORM_INPUT){backStackEntry->//拿到导航图的根entry作为ViewModelStoreOwner,实现共享valparentEntry=remember(backStackEntry){navController.getBackStackEntry(Route.FORM_INPUT)}valsharedVm:FormShareViewModel=viewModel(viewModelStoreOwner=parentEntry)FormInputScreen(navController,sharedVm)}composable(Route.FORM_PREVIEW){backStackEntry->valparentEntry=remember(backStackEntry){navController.getBackStackEntry(Route.FORM_INPUT)}valsharedVm:FormShareViewModel=viewModel(viewModelStoreOwner=parentEntry)FormPreviewScreen(navController,sharedVm)}}}@ComposablefunFormInputScreen(navController:NavHostController,vm:FormShareViewModel){valnamebyvm.nameFlow.collectAsStateWithLifecycle()valphonebyvm.phoneFlow.collectAsStateWithLifecycle()androidx.compose.foundation.layout.Column(modifier=androidx.compose.ui.Modifier.fillMaxSize(),verticalArrangement=androidx.compose.foundation.layout.Arrangement.Center,horizontalAlignment=androidx.compose.ui.Alignment.CenterHorizontally){androidx.compose.material3.Text(text="📝表单录入页")androidx.compose.material3.TextField(value=name,onValueChange={vm.updateName(it)},placeholder={androidx.compose.material3.Text("请输入姓名")})androidx.compose.material3.TextField(value=phone,onValueChange={vm.updatePhone(it)},placeholder={androidx.compose.material3.Text("请输入手机号")})androidx.compose.material3.Button(onClick={navController.navigate(Route.FORM_PREVIEW)}){androidx.compose.material3.Text("前往预览页面")}}}@ComposablefunFormPreviewScreen(navController:NavHostController,vm:FormShareViewModel){valnamebyvm.nameFlow.collectAsStateWithLifecycle()valphonebyvm.phoneFlow.collectAsStateWithLifecycle()androidx.compose.foundation.layout.Column(modifier=androidx.compose.ui.Modifier.fillMaxSize(),verticalArrangement=androidx.compose.foundation.layout.Arrangement.Center,horizontalAlignment=androidx.compose.ui.Alignment.CenterHorizontally){androidx.compose.material3.Text(text="👁表单预览页(读取共享ViewModel的数据)")androidx.compose.material3.Text(text="姓名:$name")androidx.compose.material3.Text(text="手机号:$phone")androidx.compose.material3.Button(onClick={navController.popBackStack()}){androidx.compose.material3.Text("返回录入页")}}}

逐行解读

  1. 默认隔离行为
    如果直接在每个Screen内部调用 viewModel() ;A页面、B页面会各自生成独立FormShareViewModel对象;A录入的数据B页面读取不到,数据隔离。
  2. getBackStackEntry 获取指定路由的BackStackEntry作为ViewModelStoreOwner
valparentEntry=remember(backStackEntry){navController.getBackStackEntry(Route.FORM_INPUT)}valsharedVm:FormShareViewModel=viewModel(viewModelStoreOwner=parentEntry)

viewModelStoreOwner = parentEntry 指定ViewModel的存储所有者是FORM_INPUT这条回退栈条目;
A页面与B页面都从同一个owner获取ViewModel,于是拿到同一个实例,实现数据共享。

  1. 共享ViewModel销毁时机(非常关键)
    ViewModel的生命周期绑定 FORM_INPUT 这条回退栈条目;
    只要FORM_INPUT还保留在回退栈里面,ViewModel就存活;
    当FORM_INPUT页面从回退栈中被弹出之后,共享ViewModel立刻执行clear()销毁,释放内存。
    适合分步多页表单;当用户退出表单流程,表单数据自动销毁,不需要手动清空。
  2. 业务数据流
    录入页修改Flow;预览页直接收集Flow;不需要页面之间传递Bundle参数;多页表单的数据全部保存在共享ViewModel内部。
  3. 和全局单例的区别
    不要自己写object单例类来实现页面共享数据!
    导航作用域的共享ViewModel会在退出表单流程时自动销毁;全局object会常驻内存,残留旧表单数据,容易产生bug。

容易踩坑

  1. ❌ 在子页面直接无参调用viewModel();得到独立实例,无法共享数据。
  2. ❌ 使用object全局单例做页面间数据共享;退出流程数据不会自动清空,残留脏数据。
  3. ❌ 忘记remember{}包裹getBackStackEntry;重组的时候反复获取entry,造成异常。
  4. ✅ 共享ViewModel销毁时机绑定指定路由栈条目;该条目出栈,ViewModel随即销毁。

多元化习题(练习)

习题1【基础‑改错题】

题目:多页表单A录入,B预览;直接在两个页面分别无参调用viewModel(),会出现什么问题?

valvm:FormShareViewModel=viewModel()

答案:
会生成两份独立ViewModel实例;A页面录入的数据保存到A的ViewModel;B页面拿到的是全新ViewModel;B读取不到A录入的表单数据。
解读:
默认每个composable目的地是独立ViewModelStoreOwner;实例互相隔离;想要共享必须指定同一个ViewModelStoreOwner(BackStackEntry)。

习题2【基础‑代码修改题】

现有路由: STEP_ONE=“step_one” , STEP_TWO=“step_two” ;两个页面希望共享ViewModel;写出获取parentEntry以及共享vm的代码片段。
答案:

valparentEntry=remember(backStackEntry){navController.getBackStackEntry(Route.STEP_ONE)}valsharedVm:FormShareViewModel=viewModel(viewModelStoreOwner=parentEntry)

解读:
统一以第一步页面的回退栈条目作为owner;两个页面都从该owner取出ViewModel,实现实例唯一。

习题3【进阶‑补充代码题】

题目:多页表单完成提交之后,希望直接清除整个表单流程(提交之后回退栈把step_one弹出;返回直接回到首页)写出跳转并且清理回退栈的点击逻辑。
答案:

navController.navigate(Route.HOME){popUpTo(Route.STEP_ONE){inclusive=true}}

解读:
popUpTo并且inclusive=true把step_one这条栈条目弹出;绑定于该entry的共享ViewModel会自动clear销毁;表单临时数据随之释放。

习题4【综合‑逻辑应用题】

题目:对比「导航作用域共享ViewModel」与「手写object全局单例」,说明导航共享ViewModel优势。
答案:
导航作用域共享ViewModel生命周期绑定导航回退栈条目;当该栈条目被弹出,ViewModel自动销毁,临时表单数据自动释放;
object全局单例常驻内存;上一次表单的数据会一直残留;下一次打开表单会读到脏旧数据,必须手动清空,容易遗漏。
解读:
页面流程内临时共享优先选择导航scope共享ViewModel;尽量避免手写业务全局单例。

习题5【概念简答题】

题目:共享导航ViewModel什么时候销毁?
答案:
共享ViewModel的生命周期绑定用于充当owner的那条回退栈条目;只要该路由条目还保留在回退栈,ViewModel就存活;一旦这条回退栈条目被弹出(popUpTo‑inclusive或者回退返回),ViewModel执行clear()销毁,内部viewModelScope协程全部取消。
解读:
生命周期跟随导航栈,而不是跟随Activity;这是导航共享ViewModel最核心特点。

本节课知识点总结

  1. 默认每一个composable导航页面拥有隔离独立ViewModel实例,数据互相隔离。
  2. 通过 navController.getBackStackEntry(route) 拿到指定路由的BackStackEntry作为ViewModelStoreOwner;多个页面以此owner获取ViewModel得到同一个实例,完成页面间数据共享。
  3. 共享ViewModel生命周期绑定对应的回退栈条目;条目出栈,ViewModel自动销毁释放资源。
  4. 适合分步多页表单业务场景;优先导航scope共享ViewModel,禁止使用object全局单例保存页面临时业务数据。
  5. 使用remember{}包裹getBackStackEntry,防止重组反复获取回退栈条目引发异常。

下一课预告:进阶第5课:Flow基础,StateFlow与SharedFlow区别,在ViewModel中处理网络请求

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询