第173篇 Fragment 与 ViewModel 作用域:谁该持有谁

简介: 本文深入解析Fragment与ViewModel的作用域设计本质——“状态该活多久”。厘清Fragment两段生命周期(实例/视图)与ViewModel三档作用域(Fragment级、Activity级、NavBackStackEntry级)的对应关系,指出误用导致的状态串页、内存泄漏或参数丢失等线上难题,并强调UI收集必须绑定`viewLifecycleOwner`+`repeatOnLifecycle(STARTED)`,VM严禁持有Context/View。

先把结论放在前面:Fragment 与 ViewModel 的作用域问题,本质是"状态该活多久"这一个设计决策。 Fragment 有视图生命周期(可随时重建)与实例生命周期(随返回栈存活)两段,ViewModel 有 Fragment 级、Activity 级、NavBackStackEntry 级三档。选错档位的后果不是报错,而是状态串页、内存常驻、或者参数丢失这三类难查的线上问题。

这题的区分度:只答"viewModels() 是 Fragment 级、activityViewModels() 是 Activity 级"属于背定义;能讲清两段生命周期、三档作用域的适用边界、以及"为什么 VM 里不能持 Context",就是有实践的答案。

两段生命周期与三档作用域

// 三档作用域的写法与实际边界
class DetailFragment : Fragment(R.layout.f_detail) {
   

    // ① Fragment 级:与 Fragment 实例同生共死
    private val detailVm: DetailViewModel by viewModels()

    // ② Activity 级:与宿主 Activity 同生共死,跨 Fragment 共享
    private val userVm: UserViewModel by activityViewModels()

    // ③ NavBackStackEntry 级:Navigation 场景的"页面级"作用域
    private val navVm: FormViewModel by viewModels(
        ownerProducer = {
    requireParentFragment() },
        factoryProducer = {
    defaultViewModelProviderFactory }
    )

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   
        super.onViewCreated(view, savedInstanceState)
        // 关键:收集 Flow 必须绑 viewLifecycleOwner,而不是 Fragment 本身
        viewLifecycleOwner.lifecycleScope.launch {
   
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
   
                detailVm.uiState.collect {
    render(it) }
            }
        }
    }
}

第③档在 Navigation 组件里写作 hiltNavGraphViewModels 或通过 findNavController().getBackStackEntry(...) 取。 它的存在理由是:Navigation 里每个目的地(destination)本质是一个 Fragment,它自己的 ViewModel 就是"页面级"的——从 A 页面跳到 B,A 的返回栈条目仍在,A 的 VM 存活,这与 viewModels() 的 Fragment 级语义一致,只是主体换成了 NavBackStackEntry。

两段生命周期的关键差别在这里体现:Fragment 的实例在导航离开后(未被 remove)仍然存在,但它的 View 会被销毁(onDestroyView)。所以 VM 活得更久、View 活得更短,两者的绑定必须走 viewLifecycleOwner。这一条是本篇的核心,也是最常被追问的地方。

三档作用域的选型判据

档位 生命周期 适用状态 误用的后果
viewModels() Fragment 实例 页面私有:表单草稿、列表分页状态 放到宿主级会让返回后残留旧状态
activityViewModels() Activity 实例 跨页共享:登录态、用户信息、购物车 放页面私有状态会导致切页后互相污染
NavBackStackEntry 级 返回栈条目 Navigation 的页面级状态 与 Activity 级混用会导致返回时状态错乱
// 反模式:把页面私有状态放进 Activity 级 VM
class MainActivity : AppCompatActivity() {
   
    private val vm: MainViewModel by viewModels()   // 列表滚动位置、筛选条件放在这里
}
// 后果:用户 A 页设了筛选 → 跳到 B → 返回 A,筛选条件被 B 改掉了

判据一句话:状态的归属方是谁,就挂在谁的层级上。 状态属于某个页面 → Fragment 级;状态属于整个 App 会话 → Activity 级;状态属于一个导航目的地 → 返回栈条目级。

为什么 VM 不能持有 View 或 Activity

// 错误写法一:持有 Activity Context
class BadVm(private val ctx: Activity) : ViewModel() {
   
    fun start() {
    someDialog(ctx) }        // 泄漏 Activity
}

// 错误写法二:持有 View
class BadVm2(private val v: View) : ViewModel()

// 正确:需要 Context 时用 Application
class GoodVm(app: Application) : AndroidViewModel(app) {
   
    fun label() = getApplication<Application>().getString(R.string.app_name)
}

泄漏链路的完整形态是:GC Root(单例/静态/ViewModelStore)→ ViewModel → Activity → DecorView → 整个视图树。 旋转屏幕时 Activity 被重建,旧 Activity 被 ViewModel 引用着,于是泄漏。

AndroidViewModel 为什么可以用? 因为它持有的是 Application(进程级单例),与任何具体 Activity 无关。但仍要注意别把 View 传进去。

最常见的坑是

第一层坑:把页面私有状态放进宿主级 ViewModel,返回后残留旧状态。

现场:详情页把筛选条件写进了 Activity 级 VM,用户从详情返回列表,发现列表被过滤了。根因是状态的归属方判断错。规范是先问"这个状态跟着哪个页面走",跟着页面的就挂在页面级。

第二层坑:收集 Flow 时用了 Fragment 作用域而不是 viewLifecycleOwner,视图销毁后仍在更新 UI。

// 错误写法
lifecycleScope.launch {
    vm.uiState.collect {
    binding.text.text = it } }
// binding 在 onDestroyView 后已失效 → 空指针或内存泄漏

根因是 Fragment 的 lifecycleScope 活到 onDestroy,而 binding 活到 onDestroyView。规范是用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED) 组合,它同时保证了"视图存在"与"界面可见才收集"两个条件。

第三层坑:返回栈中的 Fragment 未销毁实例,其 ViewModel 同步存活,内存常驻。

场景:导航栈里压了 5 层,每层都持有一个大列表的 VM(缓存了几 MB 数据),用户一直不返回,内存持续增长。规范是① 用 navController.popUpTo 及时清理返回栈;② 对列表类 VM 做数据上限控制;③ 用 SavedStateHandle 只存恢复状态所需的小字段,不把大列表塞进去。

这题在面试里怎么问、怎么答

问:Fragment 和 ViewModel 的生命周期是怎么绑定的?

答:Fragment 有两段——实例生命周期(onCreate 到 onDestroy,导航离开时不销毁)与视图生命周期(onCreateView 到 onDestroyView,视图重建时销毁)。by viewModels() 获取的 VM 绑在 FragmentStore 上,跟随实例生命周期;VM 持有的 ViewModelStore 在配置变更时通过 NonConfigurationInstance 保留,所以旋转屏幕不丢。关键补充:UI 相关的收集必须走 viewLifecycleOwner,因为它活到 onDestroyView。

问:viewModels()、activityViewModels()、navGraphViewModels() 有什么区别?

答:viewModels() 绑 Fragment 实例,页面私有状态用它;activityViewModels() 绑 Activity 实例,跨 Fragment 共享状态用它(登录态、购物车);Navigation 场景下 NavBackStackEntry 级作用域对应"目的地级"VM,与页面同生共死但主体是返回栈条目而非 Fragment 实例。选型判据是"状态的归属方是谁,就挂在谁的层级",选错会导致状态串页或残留。

问:为什么 ViewModel 里不能持有 Activity 或 View?

答:因为会造成内存泄漏。VM 存放在 ViewModelStore 里,而这个 Store 在配置变更时被静态的 NonConfigurationInstance 引用,等于 VM 活得比 Activity 长。若 VM 再持有 Activity,泄漏链就是 GC Root → ViewModel → Activity → 视图树。需要 Context 时用 AndroidViewModel 持 Application,且仍不得持有 View。再补一句:LeakCanary 报这类泄漏时会精确给出这条链,能自己顺着读出来是加分点。

问:repeatOnLifecycle(STARTED) 解决什么问题?

答:解决"界面不可见时仍在收集数据"。lifecycleScope.launch 本身不感知可见性,收集协程会一直运行;repeatOnLifecycle 在 ON_STOP 时取消内部协程、ON_START 时重新启动,所以每次回到前台都会拿到最新状态。它带来的正确性收益是"界面可见期间的数据始终是最新的",代价是配置变化时会重跑一次收集块,所以块里不能放一次性逻辑。

落到项目里怎么做

一条能写进规范的红线:所有 Flow 收集必须使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED) 组合,且 ViewModel 不得持有任何 View 或 Activity 引用。 这一条能拦住本篇三类最常见的问题。

配套实践三条:① 把"状态归属方"写进 VM 类的 KDoc 注释,明确这个 VM 由哪一层持有、承载哪类状态,review 时直接对照;② 给返回栈深度设上限(导航图里限制层级数,超过则 popUpTo),避免深栈内存常驻;③ 用 LeakCanary 在 Debug 包常驻,把泄漏发现提前到开发期而不是用户反馈里。

给正在准备面试的你

这题的答法要落到"状态该活多久是唯一的设计决策,两段生命周期 + 三档作用域是它的两种表达"这条主线。推荐这条线:先讲 Fragment 两段生命周期与 VM 的绑定位置 → 给三档作用域的选型判据(状态归属方是谁)→ 讲为什么 VM 不能持 Activity/View 与完整泄漏链 → 讲 viewLifecycleOwner + repeatOnLifecycle 的两个条件 → 补三个坑 → 收在"收集规范"。能被追问"旋转屏幕时 Fragment 的 View 和实例哪个活"并答出"实例活、View 重建",说明真做过状态管理;能主动画出 GC Root → ViewModel → Activity → 视图树 这条泄漏链,说明处理过泄漏排查。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:组件与-UI-阶段通关地图:从生命周期到-Compose

下一篇预告:NestedScrollView-与-CoordinatorLayout:联动滚动机制

有任何问题欢迎在评论区留言交流。

相关文章
|
17小时前
|
存储 弹性计算 运维
为什么选择阿里云服务器?价格优惠、性能稳定、生态丰富及全球地域部署多线高速网络
新手上云常困惑“哪家云服务器最好”?其实无绝对最优,只选最适合的。阿里云凭借完善生态、丰富文档成主流选择。轻量应用服务器适合新手建站、博客等简单场景,开箱即用;ECS则面向企业级应用,弹性强、扩展灵活,但需一定运维基础。按业务规模、预算与技术能力理性选择。阿里云服务器ECS官网:https://t.aliyun.com/U/AZBUsA
36 6
|
18小时前
|
XML Android开发 数据格式
第180篇 组件化 UI 收尾:从控件思维到组件思维
本文深入解析UI组件化本质:从“控件思维”跃迁至“组件思维”,核心在于将“状态+样式+行为”收敛为可复用契约。强调三条硬标准——不写死视觉token、不感知业务语义、不持有外部上下文。以Compose无状态组件与View体系实践对比,揭示状态归属是复用关键,并指出设计系统沉淀与常见落地坑。
23 0
|
19小时前
|
Android开发 容器
第177篇 软键盘与焦点适配:IME Insets 的现代方案
软键盘适配核心:摒弃`adjustResize`和固定高度猜测,改用系统Insets机制——监听并消费`WindowInsets.Type.ime()`,确保全屏/沉浸式/Compose等场景精准避让。关键在“消费”防重复、动画跟随、焦点滚动时机。现代方案唯一可靠解法。
23 0
|
18小时前
|
定位技术 Android开发 容器
第175篇 BottomSheet 与抽屉:容器类交互组件
BottomSheet 与抽屉的核心差异不在方向,而在**返回键与手势的优先级逻辑**:BottomSheet 打开时返回键应优先关闭面板;抽屉则需优先响应边缘滑动。本质是三态状态机(Hidden/PartiallyExpanded/Expanded)与嵌套滚动、返回拦截、底层交互协同的设计问题。
21 0
|
19小时前
|
缓存 编译器 定位技术
第172篇 组件与 UI 阶段通关地图:从生命周期到 Compose
本文揭示Android UI开发能力差异的核心在于**对组件默认行为的深度理解**,而非API调用本身。通过“五段能力地图”(生命周期、View体系、列表、Jetpack、Compose),每段仅设1个关键判定题(如MeasureSpec三模式、重建边界、重组机制等),聚焦“默认值何时必须改、改了有何副作用”。强调以问题驱动学习,直击面试与工程中的真实痛点。
26 0
|
19小时前
|
安全 数据可视化 测试技术
第176篇 Span 与富文本:TextView 的高级渲染
富文本本质是“在文本区间叠加样式与行为”。面试聚焦四点:Span分类选择(Character/Paragraph/MetricAffecting/Clickable)、MovementMethod与ClickSpan协同机制、性能优化(预构建复用、MetricAffectingSpan轻量、线程安全),以及Compose中AnnotatedString的双通道设计(SpanStyle+LinkAnnotation)。细节决定成败——如SPAN_INCLUSIVE_EXCLUSIVE在编辑场景的必要性、热区偏移根因、漏设movementMethod等。
23 0
|
19小时前
|
Android开发 C++ 容器
第174篇 NestedScrollView 与 CoordinatorLayout:联动滚动机制
CoordinatorLayout 与 NestedScrollView 构成“协商式”嵌套滚动协议:子View发起滚动,父View先消费(如AppBar折叠),再将剩余量回传。区别于传统“拦截式”事件分发,它按像素(dx/dy)协作、父子分工明确,核心在`onNestedPreScroll`/`onNestedScroll`等四回调及Behavior的轴判断、consumed回填、stop收尾三规范。
23 0
|
19小时前
|
XML 前端开发 区块链
第171篇 无障碍与国际化:可达性是质量的一部分
本文深入剖析无障碍与国际化的本质关联——二者实为检验“默认假设边界”的同一枚硬币两面。聚焦Android开发,系统梳理4条无障碍硬指标(触摸目标、对比度、标签、焦点顺序)及验证方法,详解RTL适配、文本膨胀、字符串拼接等国际化关键坑点,并提供可落地的CI自动化、盲测与Code Review规范。
27 0
|
1天前
|
XML 缓存 安全
第168篇 阴影与海拔:elevation 的绘制代价
Android中elevation非单纯视觉参数,而是三位一体机制:Z轴绘制顺序、阴影轮廓来源、Material表面分层依据。其核心“拐点”在于性能成本叠加——轮廓计算、离屏渲染与模糊采样。M3推荐用tonalElevation(色调分层)替代投影,兼顾性能与深色模式可读性。
26 0
|
1天前
|
API Android开发 容器
第164篇 Material 3 与动态取色:主题系统的现代化
Material 3 的本质是设计系统“换轨”:色彩转向语义角色(非固定色值),形状依托 shape scale(非硬编码圆角),动效遵循统一 motion scheme。动态取色仅是色彩轨的可选输入。核心在于保障跨主题/品牌/明暗模式的视觉一致性,代价体现在设计归类、工程收敛与混用风险上。
30 0