在这里插一条硬经验:遇到生命周期边界问题的时候,别只盯着Fragment或者Activity的某一行代码,先检查宿主方法内部的commit和事务操作是否处在isStateSaved=false的安全窗口里。这种静默失败比崩溃更难查,日志里必须留痕。”可以放置在6.4附近。
- “代码块”:代码语言指定为kotlin,这很好。
- 列表仅用于星号。
好的,在最终确定之前,让我重新查看草稿并修正几个细节。
- 标题“## 3. getActivity()强转:写出很快,崩得也快”——是个不错的标题。
- 但标题“## 2. 接口回调:官方推荐最正统,细节也最多”——还行。
- “## 6. 避坑手册:我反复踩过的7个真实雷区”——标题说“7个真实雷区”,但是4.3节有3个h3?等等,这个H2只有4个h3小节(6.1-6.4),里面列举了多个要点。标题说“7个”,但只有4个子节。让我修正。要么把子节提到7个,要么改标题为“4个”。六.3标题:“我在项目里遇到的5个真实异常”——与内容不符。改为:标题“类拾”。
让我们重新命名: “## 6. 避坑手册:我从生产环境里捡回来的真实事故” 涵盖h3:6.1, 6.2, 6.3, 6.4。不再是“7个”。
类似地,第一部分说“大致有六种”,表格有6行。好。 第二部分说“官方推荐里最正统,细节也最多”没有数字问题。
另外,我选择了“## 5. ViewModel共享作用域:把'命令Activity'变成'让Activity自己观察'”——不错。
比较多行H2标题的具体性:好。
现在让我润色最终文本。另外:第3.3节的调试叙事涉及“热修复补丁”和“……补丁把原来的……改成了及!!强制解包”。与技术相关,没有许多关联。但请确保“热修复”的引用不涉及敏感主题。安全。
还有,在6.2中,“我们使用一个静态Fragment引用缓存”——这看起来像一个抽象示例。它引用“项目”但没有命名,是来说好的。我会保持抽象。
我还应该避免任何形式的“AI生成痕迹”。比如“让我们从...开始”,也许不要。“写一段大实话”——很好,很自然。
另一个约束:“全文使用中文撰写”——我在做。
像“mentions”这样的词会过多,但我不会过度。
现在,关于合规红线,我的文章中实例是否使用真实实体?
- 类名MainActivity、OrderCardFragment、ProductFragment——这些都是示例代码中的类名,通用且虚构。
- “某同事”——虚构代称。
- “某项目”——我已经用作“某项目”,来自规则集。以后我会一直用“某项目”。
- 我在开头也用了一个场景:底部Tab加多个Activity。是通用的Android模式。
没有真名,好。
此外,确保不“为增强真实感而编造具体身份”——诸如“某同事”这样的模糊引用是可以的;我不提“XX团队”或真实公司。
出于安全考虑,我将“reference”替换为吉林(?),避免地名。所有例子均通用Android UI结构。
现在,让它更结构化一些。我担心的是“主体(≥5000字)”可能不够——字计数是以中文字符计。至9k字符草稿……超过5k,没问题。但内容是否“>=5000”公式?长度很大。我将对最终版本充满信心。
让我也确保感官自然。“我被卡住...”“我打开hallulogging...”“让我检查...” 写作风格:
进行最终汇编。我将定稿并将其作为纯Markdown发布。
还需注意:字数不得标注文末。给用户假设我的发布日期可考虑……不,不要含有元的总结,比如“全文约xx字”。避免“全文约xx字”。且刻意在结尾不引用。
所需标准:“博文的最后一个字符必须是正文内容的结尾”。我的结束段《写在后面》——听起来像一个真正的博主总结。其最后一句是:“...这两个地方的坑占比最大,其余的方案都是围绕它们展开的变体。”团队检查……这是一个个人建议的收尾,我可以接受。或者,选择以“如果你能避开这些,坑就少了一大半”结尾。但一个真实的博主会以更内省或个人的东西结尾。确保它不是“总之”或“综上所述”。“最后分享一个小技巧”的可选。看来我可以保留。
另外:“写在后面”——但它不是“AI范式的结尾总结”吗?我认为是自然的。我会调整一下,让它更个人化:“如果在团队里,我还会要求提交时把方案选择写进代码注释,至少别让后来者对着强转代码猜三个月。”
以这样的个人视角结束很好。
现在我开始为最终回答整理全文。
首先,必须从博文内容开始,无元文本。我将从开场白开始:“Fragment调用宿主Activity方法,是老生常谈...”
等一下:前100个字应该包含核心关键词。于是:“Fragment调用宿主Activity方法,是老生常谈,但每次都会有新同学踩坑。前阵子帮同事排查一个线上崩溃,定位到最后就是getActivity()强转时拿到null导致空指针。这让我想把这些年和Fragment、宿主Activity打交道的经验完整整理一遍……”
“Fragment调用宿主Activity方法”即核心关键词。排在第一个。好的。
现在我将代码和处理汇编完成。
起草时注意正确使用中文标点。
已进行最后检查。
标题层级每节H2之间没有跳级。正文自然段。代码块标注为kotlin。表格已在第一节使用。列表仅用于特定情况(比如容错列表 & 6.4 排查步骤)。这覆盖了格式要求。
一个更微妙的要点:标题检查——H2标题必须编号且具体。我有:
1. 先看本质:Fragment和宿主Activity的"生命周期差"决定N个坑
2. 接口回调:官方推荐最正统,细节也最多
3. getActivity()强转:写出很快,崩得也快
4. 嵌套Fragment跨两层调宿主:逐层透传的价值与限制
5. ViewModel共享作用域:把"命令Activity"变成"让Activity自己观察"
6. 避坑手册:我从生产环境里捡回来的真实事故
这些头2个标题当中是否有“核心细节解析”类似物?没有。它们独特且是指在本文的背景下。好。
确保H3标题格式化:每H2却有三层(1.1、2.3等)。如果为每一节只有一个连续的h3编号以避免出现歧义……采用随全局编号:1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 4.1, 4.2, 5.1, 5.2, 5.3, 6.1... 6.4. 这是按数字排列跨级别应使用的编号。是的,合理。
关于 H2 下方的第二级子标题不要求全套“至少两个H3”,声称可以包含“若干H3小节或连贯段落”。我已经包含 h3。各 h3 节字数会有所不同。
现在希望这次输出符合要求的完整发布文本。我将花额外时间润色一些过渡段和解释,以感受到真实感。
最后整理的写作。
重写几段,以适应细微改进:
开头: “Fragment调用宿主Activity方法,是老生常谈,但每次都会有新同学踩坑。前阵子帮某同事排查一个线上崩溃,定位到最后就是getActivity()强转时拿到null导致空指针。这让我想把这些年和Fragment、宿主Activity打交道的经验完整整理一遍,从原理到接口回调、强转写法、嵌套跨层、ViewModel方案,再到那些只有生产环境才会露头的奇怪问题,一次说透。”
“先把场景描述清楚”。
我将根据此思考制作最终版本。
另一点:代码块标注为kotlin——Android上可接受。
最终确定最终答案。 Fragment调用宿主Activity方法,是老生常谈,但每次都会有新同学踩坑。前阵子帮某同事排查一个线上崩溃,定位到最后就是getActivity()强转时拿到null导致空指针。这让我想把这些年和Fragment、宿主Activity打交道的经验完整整理一遍,从原理到接口回调、强转写法、嵌套跨层、ViewModel方案,再到那些只有生产环境才会露头的奇怪问题,一次说透。
先把场景描述清楚:Android里页面壳是Activity,页面内容拆成Fragment,但业务动作大多需要全局响应——切Tab、关闭当前页面、刷新角标、弹出选择器。这些动作的最终执行者基本都是宿主Activity,所以Fragment反过来“指挥”宿主Activity就成了每个项目都绕不开的操作。这件事看起来简单,实际牵扯生命周期、引用传递、内存回收、代码耦合多个维度,方案选错,轻则代码没法维护,重则线上直接崩。
1. 先看本质:Fragment和宿主Activity的"生命周期差"决定N个坑
1.1 为什么getActivity()有时拿到的不是一个"完整"的Activity
很多人把Fragment当成Activity的普通子对象,这个理解是坑的开始。Fragment由FragmentManager托管,Activity在onCreate里向FragmentManager添加Fragment,Fragment的生命周期会被延迟调度,两边的状态机完全不是同步推进的。举个例子,Activity执行到onStart时,里面某个Fragment可能还停在onAttach或者onCreate状态;反过来Activity已经进入onStop,Fragment的视图还在屏幕上,用户甚至还能正常点击。
我碰到过最典型的case:Fragment里点击按钮,在onClick里调用宿主Activity的方法,方法内部做了supportFragmentManager.beginTransaction().commit()。正常情况没问题,但如果点击时机恰好发生在Activity执行完onSaveInstanceState之后,commit会直接抛IllegalStateException。为什么?因为系统状态已经保存完毕,此时再提交Fragment事务会破坏恢复流程,框架不允许。
另一个常见场景是旋转屏幕。重建时旧Activity销毁、新Activity创建,但Fragment的重建走的是FragmentManager的非保留实例恢复机制,恢复时机比Activity的onCreate还要晚一步。如果开发者在Fragment的onCreateView里直接强转getActivity(),拿到的Activity大概率还没准备好UI。这个“生命周期差”是理解后面所有方案的前提。
1.2 一个"切Tab"需求背后的六种解法全貌
动手写代码前,应该先把手里的牌摊开。“Fragment调用宿主Activity方法”大致有六种落地方案:
| 方案 | 代码量 | 耦合度 | 生命周期安全性 | 适用场景 |
|---|---|---|---|---|
| 接口回调 | 中 | 低 | 高 | 单Activity承载多个Fragment的标准场景 |
| getActivity()强转 | 低 | 高 | 低 | 小型Demo、紧急改动、不跨宿主复用 |
| 父Fragment逐层透传 | 高 | 中 | 中 | Fragment嵌套两层以上的结构 |
| Activity Result API | 低 | 低 | 高 | 从系统能力拿结果,比如选图、拍照 |
| ViewModel共享事件 | 中 | 低 | 高 | 页面状态多、需要跨重建保存数据的场景 |
| EventBus等三方总线 | 低 | 低 | 中 | 遗留项目维护常用,新项目不建议 |
这里说句大实话:每个方案都有存在的理由,没有银弹。我见过有些团队非黑即白地禁掉getActivity()强转,结果接口回调满天飞,代码也没干净到哪去。选方案要看自己的场景,下面的选型判断标准是我个人经验,不一定适用所有人,但至少能避免无脑套方案。
2. 接口回调:官方推荐最正统,细节也最多
2.1 从HostActionListener定义到调用的完整链路
接口回调是Fragment官方文档里推荐的标准通信方式,思路是:Fragment定义接口,宿主Activity去实现,Fragment在onAttach时拿到实现接口的对象,需要触发业务时调用接口方法,具体怎么处理由宿主Activity决定。
先看完整代码。以“切换底部Tab”为例。
接口定义:
interface HostActionListener { fun switchMainTab(index: Int) fun showAddressPicker() }Fragment侧:
class OrderCardFragment : Fragment() { private var hostListener: HostActionListener? = null override fun onAttach(context: Context) { super.onAttach(context) hostListener = activity as? HostActionListener ?: context as? HostActionListener if (hostListener == null) { throw IllegalStateException("宿主必须实现 HostActionListener") } } override fun onDetach() { hostListener = null super.onDetach() } private fun onOrderClick() { hostListener?.switchMainTab(2) } }宿主Activity侧:
class MainActivity : AppCompatActivity(), HostActionListener { override fun switchMainTab(index: Int) { binding.bottomNav.selectedItemId = when (index) { 2 -> R.id.tab_order else -> R.id.tab_home } } override fun showAddressPicker() { // 弹地址选择器的逻辑 } }整个链路就是:Fragment不关心宿主Activity是谁、怎么实现,只调接口方法;宿主Activity拿到回调后负责具体动作。两边靠接口契约解耦,这是它成为官方推荐方案的核心原因。
2.2 onAttach里拿接口的正确姿势:context可能不是Activity
接口绑定第一个容易踩的细节是onAttach的context参数。从API 23开始,onAttach(Context context)里的context不一定是Activity本身,有些机型上它可能是ContextThemeWrapper等其他包装类型。我最初只做一次context转型,结果在部分机型上直接抛ClassCastException。
正确姿势是先用activity属性拿,拿不到再退回context,并且都用类型安全的as?:
hostListener = activity as? HostActionListener ?: context as? HostActionListener两种都拿不到时,我建议直接抛异常而不是静默吞掉。你想想,一个Fragment被放进没有实现接口的Activity里,这是开发期就能发现的配置错误,早崩溃比上线后功能静默失效好一万倍。当然,如果项目确实希望一个Fragment兼容多种宿主,可以允许hostListener为null,调用处统一判空,但你要接受部分功能在某些宿主机型上不生效的事实。
2.3 onDetach解绑这一行代码为什么能保命
onDetach里把hostListener置空,看起来是无所谓的清理代码,但能拦住一类非常隐蔽的内存泄漏。Fragment实例可能被FragmentManager长时间持有,如果你的hostListener是一个匿名内部类对象,而匿名内部类隐式持有Fragment引用,Fragment又被某个全局单例或EventBus缓存,整条引用链就断不掉了。宿主Activity和Fragment会一直在内存里住着,等你在LeakCanary里看到“Activity has leaked”的时候,排查起来能折腾一整天。
另外,onDetach解绑还能防住一种诡异行为:Fragment已经退出返回栈,但某个延迟任务还在运行,任务回调里调到了hostListener。没有置空的话,这个延迟任务就会操作一个已经被系统标记为detached的Activity,引发各种莫名其妙的异常。置空之后,延迟任务回调只会走到可空调用分支,什么都不做,这才是安全的失败方式。
3. getActivity()强转:写出很快,崩得也快
3.1 我什么时候会容忍这种写法
必须承认,强转写法真的省事。个人项目里Activity结构简单,Fragment只在一个宿主中使用,八成我会直接写:
(activity as? MainActivity)?.switchTab(2)运行得很好,代码也短。我自己的容忍标准只有三种情况:项目阶段是原型、Demo、一次性页面,后续不会复用这个Fragment;Fragment和宿主Activity的对应关系非常固定,全项目只有这一个入口;代码短期内就要重构或废弃。一旦进入多人协作的正式项目,强转就该被警惕了——你不是在给自己写代码,是在给下一个接手的人埋雷。
3.2 强转的两个经典崩溃形态
强转最常见的崩溃形态,不是“转错了类型”,而是压根拿不到Activity。
第一种:ClassCastException。某项目的ProductFragment原本只在MainActivity里用,后来产品要求在DetailActivity也展示同一模块,某同事直接把代码复制一份,把ProductFragment塞进去了。Fragment里的逻辑是activity as MainActivity,DetailActivity必然转换失败,用户一进详情页就崩。问题未必出在“强转”写法本身,而是这个Fragment已经被复用到多宿主环境,强转方案没有能力表达这种变化。
第二种:空指针。强转前activity属性返回null。常见时机是Fragment已经从FragmentManager移除,但Fragment对象还被某个列表或局部变量引用;或者在onDetach之后,延迟任务里又访问了activity。很多新手觉得写成activity?.switchTab(2)就安全了,其实这只是把崩溃延后了,万一后面还有activity!!.xxx,或者内部方法用到了必空参数,一样崩。
3.3 一次线上崩溃的排查复盘
记录一个我亲身参与的排查过程。某项目分享页面ShareFragment里有一句调用,让宿主刷新分享按钮状态。代码用了强转:
(activity as ShareActivity).updateShareButton(true)线上crash日志显示大量ClassCastException,但奇怪的是直接进分享页不复现。翻日志后发现规律:崩溃集中在“分享页→跳转用户详情页→再返回分享页”这条路径。返回的时候ShareFragment被重建,恢复过程中Fragment先attach到Activity,紧接着某个回调里用了强转,崩溃栈却不在Fragment内部。
细查崩溃栈才发现,崩溃发生在生产环境的热修复补丁逻辑里——补丁把原来的(activity as ShareActivity)改成了(activity as? ShareActivity),后面却紧跟着一句!!.updateShareButton(...),于是ClassCastException变成KotlinNullPointerException。这个案例给我们的教训不是“强转不能碰”,而是“改动老代码要连同调用时机一起review”,尤其不能随意替换生命周期回调附近的类型转换逻辑。
4. 嵌套Fragment跨两层调宿主:逐层透传的价值与限制
4.1 用requireParentFragment()做逐层透传
现在的项目结构普遍是Activity→外层Fragment→子Fragment,尤其使用ViewPager2或Navigation嵌套导航时,一个页面里能有三层Fragment。子Fragment要调宿主Activity方法,比较朴素的做法是逐层透传:子Fragment用requireParentFragment()拿到父Fragment,父Fragment再通过接口或强转调用宿主Activity。
代码长这样:
class ParentFragment : Fragment(), HostActionListener { override fun switchMainTab(index: Int) { (requireActivity() as? HostActionListener)?.switchMainTab(index) } } class ChildFragment : Fragment() { private val hostListener: HostActionListener? get() = (requireParentFragment() as? HostActionListener) ?: (requireActivity() as? HostActionListener) private fun onButtonClick() { hostListener?.switchMainTab(2) } }这里有个细节:父Fragment实现了HostActionListener,方法体里再调requireActivity()上的接口。这么做的原因是让子Fragment不关心父Fragment之下还有没有别的层,它只要面向“最近的实现了接口的宿主”即可。兜底逻辑是当父Fragment没实现接口时直接退回宿主Activity判断。不管嵌套几层,子Fragment的调用点都能保持单点。
限制也很明显:每层都得做一次接口实现和转发,层数一多样板代码剧增,还容易出“某层忘了转发”的低级bug。三层以上我基本直接劝人上ViewModel。
4.2 Activity Result API如何"砍掉"部分多余调用
有些“Fragment调用宿主Activity方法”的需求,本质不是真要让宿主做点什么,而是Fragment希望拿到一个外部结果——选一张图、拍一张照、调起文件选择器。这种场景与其在宿主Activity里写一个openSystemPicker(),不如让Fragment自己用Activity Result API启动系统页面并接收结果。
实现很简洁:
class AvatarFragment : Fragment() { private val pickImage = registerForActivityResult( ActivityResultContracts.GetContent() ) { uri: Uri? -> uri?.let { uploadAvatar(it) } } private fun onPickClick() { pickImage.launch("image/*") } }宿主Activity完全不用暴露方法,Fragment自己就能和系统交互。这样既砍掉一部分宿主Activity方法的必要性,又顺带规避了生命周期问题——Activity Result API在Fragment重建时会自动重新分发结果,不用手动补逻辑。我建议读者把项目里所有“为了调系统能力而专门写宿主方法”的代码都换成这种模式。
5. ViewModel共享作用域:把"命令Activity"变成"让Activity自己观察"
5.1 为什么同一个Activity作用域下的ViewModel能跨过重建存活
ViewModel是另一个更现代的选择,核心思路是把“Fragment直接调Activity方法”改写成“Fragment修改共享状态,Activity自己观察状态并作出响应”。做到这件事的关键,是让Fragment和Activity拿到同一个ViewModel实例。
Activity里用by viewModels(),Fragment里用by activityViewModels(),两者的ViewModelStore最终都挂在这个Activity上。当Activity因为配置变更被系统销毁重建时,ViewModel不会被销毁,而是由系统保留到下一次Activity创建后重新绑定。这个机制让Fragment在旋转屏幕后仍能拿到同一个共享ViewModel,页面状态也因此不会丢失。
5.2 用Flow发送一次性事件的完整写法
这是一套我在正式项目里用过的模板。共享ViewModel:
sealed class MainTabEvent { data class SwitchTab(val index: Int) : MainTabEvent() object ShowAddressPicker : MainTabEvent() } class MainSharedViewModel : ViewModel() { private val _events = Channel<MainTabEvent>(Channel.BUFFERED) val events = _events.receiveAsFlow() fun dispatch(event: MainTabEvent) { viewModelScope.launch { _events.send(event) } } }宿主Activity侧:
class MainActivity : AppCompatActivity() { private val sharedModel: MainSharedViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { sharedModel.events.collect { event -> handleEvent(event) } } } private fun handleEvent(event: MainTabEvent) { when (event) { is MainTabEvent.SwitchTab -> switchTab(event.index) MainTabEvent.ShowAddressPicker -> showAddressPicker() } } }Fragment侧:
class OrderCardFragment : Fragment() { private val sharedModel: MainSharedViewModel by activityViewModels() private fun onOrderClick() { sharedModel.dispatch(MainTabEvent.SwitchTab(2)) } }这里选Channel而不是LiveData是有讲究的。LiveData会一直保存最新值,新的观察者一旦开始观察,立刻收到当前值。如果你用LiveData传“切换Tab”这种一次性事件,那么旋转重建之后Activity重新开始观察,会重复收到上一次事件,导致Tab被无意义地再切一次。Channel配合receiveAsFlow就没有重放问题,发送完就完了。这个细节我交过学费,读者千万记住。
5.3 接口回调和ViewModel的分工与共存
ViewModel不是万能的。它适合“触发一次单向动作”的场景,比如导航、弹窗、刷新标记。但如果你想“调用Activity方法,并且立刻拿到它的处理结果”,用ViewModel就很别扭,因为事件是异步的,你得另做一套请求-响应机制。这种同步调用场景,接口回调依然是首选。
我现在项目的用法是两种混用:常规同步联动、简单动作走接口回调;涉及页面状态、多次重建、深层嵌套时走ViewModel事件。关键别在同一个类里塞两种写法处理同一件事,过度设计比不用方案更难受。
6. 避坑手册:我从生产环境里捡回来的真实事故
6.1 不要在Fragment的onCreate阶段去调宿主Activity方法
有人在新写的Fragment的onCreate里直接调用宿主Activity的某个初始化方法,结果宿主Activity里要用的字段还没初始化,当场空指针。原因在于Fragment的onCreate回调时机可能早于Activity,也可能晚于Activity,完全取决于FragmentManager的调度顺序。不要假设“宿主一定初始化完成”,除非你明确知道添加Fragment的时机。真要调用就放到onStart之后,配合lifecycleScope启动,让生命周期保证时序。
6.2 配置变更后"回调失效",多半不是接口绑定的问题
有段时间我发现某个Fragment旋转屏幕后,点按钮调宿主方法没反应。开始怀疑接口绑定出问题,因为Activity和Fragment都重建了。打印日志发现onAttach照常执行,绑定也没断,但宿主Activity的方法就是没被调用。接着检查Fragment本身的引用,发现保存的是一个旧Fragment对象,它里面的hostListener还指向旧Activity。根源是我们用了一个静态的Fragment引用缓存,旋转时没有清理,重新生成的新Fragment虽然正确,点击事件却走的是旧对象。把静态缓存去掉,问题消失。这类问题本质是引用了不该缓存的Activity/Fragment对象。
6.3 用单例或EventBus持有Fragment的回调会导致泄漏
接口回调方案里,如果把hostListener交给一个单例,或者注册到全局EventBus,记得在onDetach时解绑并把引用置空。某项目里有人把Fragment写的匿名回调注册到全局监听器,导致即使Fragment离开了返回栈,监听器依然引用整个Fragment,连带Activity一起泄漏。LeakCanary一抓一个准。规避方式就两条:能不用全局监听就不用;无状态回调尽量写成顶层函数或静态内部类,不要让它在内部隐式持有外部实例。
6.4 排查实录:一次"宿主方法明明被调了却没反应"的完整链路
最后复盘一个真实问题。场景:点Fragment里的按钮,宿主Activity的switchTab()方法入口处打了断点,能走进去,但Tab就是不切。开始怀疑视图刷新顺序,检查了setSelectedItemId的调用,没问题。后来打开日志发现异常被吞了——方法内部有一段runCatching包裹的代码,里面调用了supportFragmentManager.commit(),但调用发生时宿主Activity已经处于onSaveInstanceState之后,commit被框架拒绝,抛出的IllegalStateException又被runCatching吞掉,从外面看就像什么都没发生。
完整排查链路:
- 先确认Fragment的点击事件确实执行到了宿主方法入口;
- 在宿主方法里逐步执行,定位到异常代码块;
- 把runCatching临时改成打印完整异常日志,看到IllegalStateException;
- 分析原因:事件触发时机晚于状态保存,属于典型生命周期边界问题;
- 修复:在commit之前增加
if (!isStateSaved)判断,或者把操作推迟到下一个生命周期执行。
这类静默失败最难查,表面看方法调了、代码也执行了,就是没效果。我现在的习惯是,所有包了runCatching或try/catch吞异常的地方,都留一条Log.w日志,至少别让问题完全隐身。说白了一个崩溃日志都比无言沉默好定位十倍。
以上是我在Fragment和宿主Activity通信这潭水里踩过和填平的坑。我目前的标准配置是:简单页面接口回调,嵌套超过三层或涉及重建用ViewModel事件,能走Activity Result API的系统能力操作绝不在宿主里写多余方法。你不需要一次记住所有细节,但建议把接口回调和避坑这两部分反复看看,这两个地方的坑占比最大,其它方案基本是围绕它们展开的变体。如果在团队里,最好把每次方案选择的原因写进commit信息里,给后来者省点挖坑的时间。