Android 基础补强 B08|Fragment 还在,为什么 binding 已经不能用了
摘要:用列表进入详情再返回的过程,理解 Fragment 事务、返回栈、View 生命周期及结果通信,避免对旧 View 更新和重复添加页面。
标签:Android、Fragment、生命周期、ViewBinding、第一行代码
本文对应《第一行代码》第 3 版第 5 章,以及 28 天课程 D17 的传统界面练习。Fragment 是受宿主管理的界面组件,但 Fragment 对象存在和它的 View 存在不是同一件事。下文使用独立的 FragmentActivity 练习入口,不改主项目的 Navigation 3;代码片段需要补齐布局、依赖和工厂,尚未编译。
1. 用返回操作理解两段寿命
列表 Fragment A 进入详情 B,如果事务允许返回,A 可能仍由 FragmentManager 保留,而它之前创建的 View 已被销毁。返回时 A 可以重新创建 View。于是成员变量 _binding 若一直持有旧 Binding,就等于持续引用旧界面树;异步回调还可能更新一份已经不显示的控件。
Fragment 生命周期描述组件本身,View 生命周期描述本次界面实例。访问 Binding 应限制在 View 创建之后、销毁之前。页面的业务数据可以放在 ViewModel 或 Repository,控件引用不能跟随较长寿命的业务对象一起保存。Fragment 生命周期
这也解释了为什么仅把 Binding 声明成可空还不够。需要在销毁时释放引用,让收集界面状态的任务跟随 viewLifecycleOwner,并避免长期对象保留 View、Activity 或带着它们的闭包。空安全和对象寿命是两类问题。
2. 一次 View 创建对应一次收集关系
假设 fragment_articles.xml 生成 FragmentArticlesBinding,包含列表 articleList;articleAdapter 来自 B07,viewModel.state 发出不可变列表状态。以下代码位于 Fragment 内,依赖 Fragment、Lifecycle、RecyclerView 与 ViewBinding。
kotlin
private var binding: FragmentArticlesBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val current = FragmentArticlesBinding.inflate(inflater, container, false)
binding = current
return current.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val current = checkNotNull(binding)
current.articleList.layoutManager = LinearLayoutManager(requireContext())
current.articleList.adapter = articleAdapter
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { articleAdapter.submitList(it.items) }
}
}
}
override fun onDestroyView() {
binding?.articleList?.adapter = null
binding = null
super.onDestroyView()
}
生命周期低于指定状态时内部收集停止,重新达到状态时再启动。重新收集不应自动等同于重复发送网络请求,是否刷新由数据层策略决定。上例把 Adapter 从旧 RecyclerView 脱离,也让引用边界更清晰;如果 Adapter 本身长期持有旧 Activity,仍需要修复,清一个字段不是万能证明。
3. 事务与返回栈需要显式说明
Activity 首次创建练习入口时,只在 savedInstanceState == null 时添加初始 Fragment,因为恢复时 FragmentManager 可以恢复已有组件。无条件重复添加可能导致旋转后出现重叠或重复实例。进入详情用事务替换容器,若期望返回列表,要加入返回栈。
下面假设宿主布局包含 FragmentContainerView,ID 为 fragment_container,使用 Fragment KTX 的类形式操作。代码放在有效的用户交互时刻,不能拿它解决宿主已经保存状态后的任意异步导航。
kotlin
supportFragmentManager.commit {
setReorderingAllowed(true)
replace<ArticleDetailFragment>(
R.id.fragment_container,
args = bundleOf("articleId" to selectedId)
)
addToBackStack("article_detail")
}
commit 安排事务稍后执行,不意味着下一行就能找到新 View。commitNow 是另一种即时提交方式,并不能与加入返回栈随意混用。遇到状态已保存后的提交问题,应检查触发时机与恢复策略,而不是默认改成允许状态丢失。Fragment 事务说明
传入文章 ID 让详情按身份查询数据,避免把可变的大对象与控件引用塞进参数。无效 ID 应进入明确的不可用页面,这样恢复过程和正常点击过程可以使用相同入口。
4. 通信先区分持续状态与一次结果
列表和详情共享收藏状态,适合读取同一个 Repository 或合适作用域的 ViewModel。详情不必持有列表 Fragment,然后直接寻找列表按钮改色。对于"选择完一个筛选项返回"的一次结果,可以使用 Fragment Result API;发送方和监听方必须使用同一个 FragmentManager 和同一个键。Fragment 通信指南
监听对象达到可交付的生命周期状态后才处理结果。结果键并不是持久消息队列,同一键在尚未交付时重复设置可能只保留最新结果。因此不要用它保存整个收藏历史,也不要把"发送过一次"当作数据库已经持久化。
共享 ViewModel 同样需要明确作用域。Activity 级共享可能适合两个同宿主页面,但范围过大也会让无关页面共享状态。选择范围时应回答"谁需要共同读写这份状态、什么时候应该结束",而不是只为了方便调用。
5. 进入、返回、旋转的实验顺序
给 Fragment 和每次创建的 root 记录不同实例标识。列表进入详情再返回,预期能观察到组件与 View 寿命并非总是同步;旧 View 的收集在销毁后结束,新 View 有自己的收集。重复十次,检查是否出现重复观察和旧对象更新日志。
随后在列表旋转设备,确认不会重复添加初始 Fragment。最后从详情修改收藏再返回,预期列表通过共享数据源显示最新状态,而不是依赖保存下来的 View 引用。若结果不一致,先查询数据,再检查作用域及收集生命周期。以上为实验预期,实际日志需要自行执行后补充。
6. 原创面试问答与追问
问一:Fragment 没销毁,为什么要清 Binding? 它的 View 可能已销毁,Binding 引用的是那次 View。追问:只用 Fragment 的生命周期收集够吗?界面更新应绑定 View 的生命周期。
问二:commit 之后能立即操作新控件吗? 不能假定事务已经执行。追问:是否统一换 commitNow?不应,返回栈与时机约束不同,应在组件正常 View 回调中初始化。
问三:Fragment Result 和共享 ViewModel 怎么选? 一次结果与持续共享状态需求不同。追问:收藏归属哪个?长期事实应由数据层持久化,共享状态持有者组织展示。
问答为原创整理,并非面试鸭题库原题。验收时能画出一条进入、返回与 View 重建时间线,解释引用何时有效、事务怎样恢复、数据如何共享,就完成了本篇第 5 章基础补强。