21、Fragment:Fragment生命周期、FragmentManager、DialogFragment与BottomSheetDialog

Fragment,这个从Android早期就存在的组件,说实话,很多人对它又爱又恨。爱的是它让界面复用变得简单,恨的是它的生命周期实在太绕了。我在项目中见过不少因为Fragment生命周期没处理好导致的崩溃,比如空指针、状态丢失、界面闪烁……嗯,今天我们就把它彻底讲清楚。

Fragment的生命周期:比Activity更复杂的存在

Fragment的生命周期方法比Activity多得多。为什么?因为它要依附于Activity,还要考虑被添加、移除、替换、回退等各种场景。我个人习惯把Fragment的生命周期分成三个阶段来看:

  • 创建阶段:onAttach → onCreate → onCreateView → onViewCreated → onActivityCreated → onStart → onResume
  • 运行阶段:onResume ↔ onPause(可见性变化)
  • 销毁阶段:onPause → onStop → onDestroyView → onDestroy → onDetach

核心要点:onCreateView和onDestroyView是成对出现的。每次Fragment的视图被创建或销毁时都会调用它们。这意味着如果你的Fragment被添加到回退栈然后又回来,onCreateView会再次执行。

我在项目中遇到过一个问题:在onCreateView里初始化了一个列表适配器,结果Fragment从回退栈回来时,适配器里的数据全丢了。为什么?因为onCreateView重新执行了,但之前的数据没有恢复。后来我学乖了,把数据初始化放在onCreate里,视图相关放在onCreateView里。

我的建议:把数据加载和状态恢复放在onCreate中,把视图绑定放在onViewCreated中。onCreateView只做布局填充,别干别的。

来看一个标准的Fragment生命周期代码示例:

class MyFragment : Fragment() {

    override fun onAttach(context: Context) {
        super.onAttach(context)
        // Fragment与Activity关联,context可用
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 数据初始化,状态恢复
        // 注意:此时视图还没创建
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 只做布局填充
        return inflater.inflate(R.layout.fragment_my, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 视图已创建,可以绑定控件、设置监听
        // 我个人习惯把findViewById和点击事件写在这里
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // 视图即将销毁,释放视图相关资源
        // 比如取消图片加载、清空引用等
    }

    override fun onDetach() {
        super.onDetach()
        // Fragment与Activity解绑
    }
}

注意:onDestroyView之后,Fragment的视图被销毁,但Fragment实例还在。如果你在onDestroyView之后还持有视图引用,就会造成内存泄漏。我曾经因为这个被OOM坑过好几次。

FragmentManager:Fragment的管家

FragmentManager负责管理Fragment的添加、移除、替换、回退等操作。说白了,它就是Fragment的大管家。每个Activity或Fragment都有自己的FragmentManager。

你想想看,如果不用FragmentManager,你怎么把Fragment显示到屏幕上?手动new一个然后setContentView?显然不行。FragmentManager帮我们处理了生命周期、事务提交、状态保存等复杂逻辑。

常用的操作有这些:

  • add():添加Fragment,不会移除已有的
  • replace():替换当前Fragment,会移除已有的
  • remove():移除Fragment
  • hide()/show():隐藏/显示Fragment,不销毁视图
  • addToBackStack():添加到回退栈

来看一个典型的事务提交:

supportFragmentManager.commit {
    replace(R.id.fragment_container, MyFragment())
    addToBackStack(null) // 添加到回退栈
}

这里有个坑:commit是异步的。它会在主线程的下一个空闲时机执行。如果你需要立即执行,可以用commitNow()。但要注意,commitNow()不支持回退栈。

避坑指南:我曾经在onSaveInstanceState之后提交事务,结果抛出了IllegalStateException。因为状态已经保存,不能再提交事务。解决方案是在onCreate中恢复状态时再提交,或者使用commitAllowingStateLoss()——但这个方法要慎用,因为它会丢失状态。

DialogFragment:比Dialog更优雅的选择

很多人还在用AlertDialog,其实DialogFragment才是官方推荐的方式。为什么?因为它有自己的生命周期,能正确处理屏幕旋转和状态恢复。说白了,DialogFragment就是一个以对话框形式展示的Fragment。

DialogFragment的生命周期和普通Fragment基本一致,但多了几个方法:

  • onCreateDialog():创建对话框,可以返回自定义的Dialog
  • setupDialog():设置对话框的样式和属性
  • dismiss():关闭对话框

来看一个简单的DialogFragment示例:

class ConfirmDialogFragment : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return AlertDialog.Builder(requireContext())
            .setTitle("确认删除")
            .setMessage("确定要删除这条记录吗?")
            .setPositiveButton("确定") { _, _ ->
                // 执行删除操作
            }
            .setNegativeButton("取消", null)
            .create()
    }
}

// 使用
ConfirmDialogFragment().show(supportFragmentManager, "confirm_dialog")

关键点:DialogFragment的show()方法接收一个FragmentManager和一个tag。tag用于在屏幕旋转后恢复对话框。如果你不传tag,旋转后对话框会消失。

我在项目中遇到过一个问题:DialogFragment显示后,点击返回键直接关闭了整个Activity。为什么?因为我没有处理返回键事件。解决方案是在onCreateDialog中设置:

dialog?.setCanceledOnTouchOutside(true)
dialog?.setOnKeyListener { _, keyCode, _ ->
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        dismiss()
        true
    } else false
}

BottomSheetDialog:底部弹出的优雅方案

BottomSheetDialog是DialogFragment的一个子类,专门用于实现底部弹出的对话框。它继承自AppCompatDialogFragment,所以也拥有完整的生命周期管理。

使用起来很简单:

class MyBottomSheetDialog : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_bottom_sheet, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 绑定控件,设置数据
    }
}

// 使用
MyBottomSheetDialog().show(supportFragmentManager, "bottom_sheet")

BottomSheetDialog有几个重要的特性:

  • 可拖拽:用户可以通过手势上下拖动
  • 两种状态:展开和折叠
  • 可设置高度:通过设置peekHeight控制折叠时的高度
  • 可禁用拖拽:通过设置isDraggable为false

注意:BottomSheetDialog默认是半透明的,背景会变暗。如果你不想让背景变暗,可以在onCreate中设置:setStyle(STYLE_NO_TITLE, R.style.Theme_Design_BottomSheetDialog)。另外,如果内容太少,它可能不会完全展开,需要设置behavior的skipCollapsed为true。

我曾经在项目中用BottomSheetDialog做了一个分享面板,效果非常好。但有个坑:如果Fragment的宿主Activity被销毁重建,BottomSheetDialog的状态不会自动恢复。解决方案是在onSaveInstanceState中保存状态,然后在onCreate中恢复。

知识体系总览

下面这张图总结了Fragment相关的核心知识点,包括生命周期、FragmentManager、DialogFragment和BottomSheetDialog的关系:

Fragment知识体系 Fragment 生命周期:onAttach → onCreate → onCreateView → onStart → onResume FragmentManager:add / replace / remove / hide / show DialogFragment:onCreateDialog / show / dismiss / 生命周期管理 BottomSheetDialog:可拖拽 / 展开折叠 / peekHeight / isDraggable

总结一下:Fragment的生命周期比Activity复杂,但掌握了规律就不难。FragmentManager是操作Fragment的核心工具,事务提交要注意异步和状态保存。DialogFragment和BottomSheetDialog是官方推荐的对话框方案,比传统Dialog更健壮。嗯,这些知识点在实际项目中几乎天天用到,希望你能真正理解它们。


公众号:蓝海资料掘金营,微信deep3321