Android 笔记:Fragment
Fragment 是一个类似 Activity 的 UI 界面,但它只是界面的一部分。
要显示 Fragment 的内容,必须将其放置在一个 Activity 里。Fragment 无法直接在界面上显示。一个 Activity 里可以放置许多个 Fragment。
Fragment 可以嵌套。
Fragment 有自己的生命周期,它的生命周期同时也受它所在的容器(Activity 或另一个 Fragment)的生命周期影响。
生命周期
Fragment 与 Activity 有着类似的生命周期。不同的是,Fragment 的生命周期回调方法是 public
的,而 Activity 的生命周期回调方法是 protected
的。这是因为 Fragment 的生命周期回调方法需要被其它 Activity 或 Fragment 调用。
Fragment 的视图填充与 Activity 也不一样,Fragment 的视图填充(inflate)不在 onCreate(Bundle?)
回调方法里,而是在 onCreateView(LayoutInflater, ViewGroup?, Bundle?)
回调方法里。
class CrimeDetailFragment : Fragment() {
private lateinit var crime: Crime
private lateinit var binding: FragmentCrimeDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
crime = Crime(id = UUID.randomUUID(),title = "", date = Date(), isSolved = false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCrimeDetailBinding.inflate(inflater, container, false)
return binding.root
}
}
同 Activity 一样,在 Fragment 中也可以使用 View Binding。在上面的 inflate()
方法调用中,第一个参数与 Activity 一样。第二个参数是该 Fragment 的父级容器。第三个参数表示是否要立即将填充好的 Fragment 视图添加到父级容器里。这里使用 false
是因为 Activity 会自动地做这件事。
onAttach(Context?)
被添加到父级容器时执行。可以通过参数获取到父级容器实例。
onCreate(Bundle?)
在程序员视角来看,相当于构造方法,用于初始化一些非视图的数据和组件,获取通过参数 bundle
传递给 Fragment 的数据。此时不应该进行任何与 UI 相关的操作。
onCreateView(Inflater, ViewGroup?, Bundle?)
在 onCreate()
之后调用。用于创建并返回 Fragment 的视图。
onViewCreated(View, Bundle?)
在 onCreateView()
之后调用。视图已经创建完成,可以进行视图的初始化操作,例如:绑定视图、设置事件等视图相关逻辑。
- (观察到一个现象,屏幕旋转时, Fragment 实例会被创建好几次,可能最后只留下其中一个,但是如果在回调方法里写一些改变外部状态的逻辑的话,可能会重复被执行好几次,导致期望之外的结果。)
使用 Fragment
要将写好的 Fragment 添加到 Activity 或者其它 Fragment 上,只需要在布局文件中通过 <FragmentContainerView>
引入即可:
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_container"
android:name="me.duron600.criminalintent.CrimeDetailFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"/>
这里的 android:name
属性就是 Fragment 的完整名称。
FragmentManager
除了用以上静态的方式在 Activity 中添加 Fragment 以外,还可以通过代码动态地添加 Fragment。
在 Activity 中,可以通过 supportFragmentManager
获取到 FragmentManager,开启一个事务,然后创建出一个 Fragment 实例,添加到指定 id 的窗口上,最后提交:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"/>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val fragment = CrimeDetailFragment()
supportFragmentManager.beginTransaction().add(R.id.main, fragment).commit()
}
}
对 Fragment 的添加、删除、替换等操作都必须通过事务来完成。在一个事务中可以批量处理一组 Fragment 相关操作。FragmentManager 维护着一个回退栈,栈中存放着这些事务。
Fragment 的生命周期由 FragmentManager 管理,而非像 Activity 那样由操作系统管理。操作系统对 Fragment 一无所知。
Fragment 的内存管理
用户通过 UI 操作在不同的 Fragment 之间切换时,FragmentManager 会保留着各个 Fragment 的实例,但会销毁那些不显示在界面上的 Fragment 的视图。在销毁视图时,会调用回调方法 onDestroyView()
。当视图变得可见时,又会调用回调方法 onCreateView(...)
。
使用 View Binding 时
但是在使用 View Binding 的时候,binding 对象会持有视图的引用,因此会导致 FragmentManager 认为该视图还被代码所需要,从而阻止视图的销毁。
在这种情况下,我们需要手动在 onDestroyView()
里将 binding 对象赋值为 null:
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
同时,需要把这个 binding
声明为可空的:
class CrimeListFragment : Fragment() {
private var binding: FragmentCrimeListBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCrimeListBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
书上用的是另一种写法,我其实不太明白这样写的必要性:
class CrimeListFragment : Fragment() {
private var _binding: FragmentCrimeListBinding? = null
private val binding
get() = checkNotNull(_binding) {
"Cannot access binding because it is null. Is the view visible?"
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCrimeListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Fragment 中的 ViewModel
与 Activity 中的 ViewModel 一样,它只有两种状态:已创建和已销毁。它的生命周期依赖 Fragment 而非 Activity,假如 Fragment 被销毁,相应的 ViewModel 也会被销毁。
如果 Fragment 随着事务被加入回退栈,Fragment 实例和它的 ViewModel 不会被销毁。这种情况下,如果用户按下回退按钮,Fragment 界面会回到屏幕上,并且 ViewModel 中所有的数据都保留着。