Android Fragment 使用及浅析
Fragment 是在 Android 3.0 (API level 11) 开始引入的。每个 Fragment 拥有自己的布局以及生命周期。
Fragment不能独立存在,必须依赖于Activity。一个Activity里可以有多个Fragment,并且一个Fragment可以被多个Activity重用。
Fragment 的基本使用
首先需要创建一个 Fragment,代码如下:
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
class BlankFragment : Fragment(R.layout.fragment_blank) {
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
companion object {
fun newInstance(param1: String, param2: String) =
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
在 Androidx fragment 1.1.0 后,我们可以使用将 layoutId 作为参数的 Fragment 构造函数,这样就不需要重写 onCreateView 方法了,例如:
class BlankFragment : Fragment(R.layout.fragment_blank) {
}
接下来编写 Activity 代码,并在 onCreate() 中添加一个 Fragment,代码如下:
class TestActivity : AppCompatActivity() {
private val binding by lazy(LazyThreadSafetyMode.NONE) {
ActivityTestBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
supportFragmentManager.commit {
add(R.id.frag, BlankFragment.newInstance("p1", "p2"))
}
}
}
Activity 对应的 xml 布局代码如下,里面编写了一个 FragmentContainerView ,这是:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
ok,通过以上代码,我们就在 Activity上能正常展示 Fragment 了。
Fragment 的容器
过去我们使用 FrameLayout 作为 Fragment 的容器,在 AndroidX Fragment 1.2.0 后,我们可以使用 FragmentContainerView 来代替 FrameLayout 。
FragmentContainerView 是专门为 Fragment 设计的 View,它继承自 FrameLayout。
同时,FragmentContainerView 提供了一些属性,能够让我们更够已静态代码的方式来展示 Fragment,
- android:name:可以指定容器上需要添加的 Fragment
- android:tag:可以为 Fragment 设置 tag
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.MyFragment"
android:tag="my_tag">
</androidx.fragment.app.FragmentContainerView>
FragmentManager
FragmentManager 负责管理 Activity 或 Fragment 中的 所有 Fragment,它是一个抽象类,用于创建 添加,移除,替换 等 Fragment 的事务(Transaction )。
想要动态的添加一个 Fragment,第一步就是要获取 FragmentManager 。
那么,如何获取 FragmentManager呢 ?
- 调用 FragmentActivity 的
getSupportFragmentManager() 方法:返回 Activity 的 FragmentManager ,当 Activity 需要嵌套 Fragment 时调用。 - 调用 Fragment 的
getChildFragmentManager() 方法:返回 当前Fragment 的 FragmentManager ,当 当前Fragment 需要嵌套 子Fragment 时调用。 - 调用 Fragment 的
getParentFragmentManager() 方法:如果当前 Fragment 附属于 Activity ,则该方法返回的是 Activity 的 FragmentManager ;如果当前 Fragment 是另一个 Fragment 的 子Fragment ,则返回的是其 父Fragment 的 ChildFragmentManager。
已弃用 API :
1、 Activity 的 getFragmentManager() 方法
2、 Fragment 的 requireFragmentManager() 方法
3、 Fragment 的 getFragmentManager() 方法
FragmentTranscation
在 FragmentManager 中,对 Fragment 的所有操作都是通过 FragmentTransaction 来执行的。
添加 / 移除 操作
add() 是 Fragment 众多操作中的一种,与之同级的方法还有remove() , replace() , hide() 等,
add() 第一个参数是承载 Fragment 的容器的 id ,第二个参数是Fragment对象,第三个参数是fragment的 Tag 名。
指定 Tag 的好处是后续我们可以通过以下代码从 FragmentManager 中取得 Fragment 对象:
val mFragment = supportFragmentManager.findFragmentByTag("my_fragment_tag")
在一次事务中,可以做多个操作,比如同时做add() / remove() / replace() 。
remove() 方法会使 Fragment 的生命周期执行完 onDetach,之后Fragment的实例也会从 FragmentManager 中移除。
add 和 replace 的区别
add() 会将一个 Fragment 添加到容器顶部,且不会移除之前已添加的 Fragmentreplace() 会替换容器顶部的一个 Fragment
如果当前已有一个 FragmentA 并且通过 add() 方法添加了一个 FragmentB ,则 FragmentA 将仍处于活动状态,因此,按下返回按钮时, FragmentA 不会调用 onCreateView方法 。
如果创建了 FragmentB 并 replace 顶部的 FragmentA ,则 FragmentA 将被从容器中删除(这时 FragmentA 会执行onDestroy),而 FragmentB 将位于顶部。
另外,如果我们的应用有内存限制,我们需要考虑使用 replace 而不是 add。
提交 操作
每次事务,我们可以通过commit() 方法进行提交。
commit() 操作是异步的,内部通过mManager.enqueueAction() 加入处理队列。
commit() 对应的同步方法为commitNow() 。
commit() 内部会有checkStateLoss() 操作,如果我们使用不当,比如 commit() 操作在 onSaveInstanceState() 之后,将会会抛出异常,而 commitAllowingStateLoss() 方法则是不会抛出异常版本的commit() 方法,但是我们还是应该尽量使用commit() ,将开发中的问题提前暴露出来。
添加回退栈
FragmentManager 拥有回退栈(即BackStack),类似于Activity的任务栈。
通过 addToBackStack() 这个方法可以将当前事务加入回退栈,当用户点击返回按钮,会回退该事务,
回退指的是如果事务是add(),那么回退操作就是remove()
而如果我们没添加 addToBackStack() 方法,则用户点击返回按钮会直接销毁 Activity 。
Fragment 的生命周期
Fragment 的生命周期方法一共有 11 个,如图所示:
我们来看看他们分别对应于什么情况,以及他们的作用:
1、onAttach(context: Context)
方法回调时,说明 Fragment 与 Activity 已完成绑定,此时候我们可以执行mContext = context 的操作,且可以正常拿到 Activity 的上下文。
2、onCreate(Bundle savedInstanceState)
可用于初始化Fragment,另外可以通过参数 savedInstanceState 获取之前状态
3、onCreateView()
初始化 Fragment 的布局,加载布局和 FindViewById 的操作通常在此函数内完成。不建议在此执行耗时的操作。
4、onActivityCreated()
执行该方法时,与 Fragment 绑定的 Activity 的 onCreate 方法已经执行完成并返回,在该方法内可以进行与 Activity 交互的 UI 操作。
5、onStart()
和 Activity 一致,执行该方法时,Fragment由不可见变为可见状态。
6、onResume()
执行该方法时,Fragment 处于活动状态,用户可与之交互。
7、onPause()
执行该方法时,Fragment处于暂停状态,但依然可见,用户不能与之交互。
8、onStop()
执行该方法时,Fragment完全不可见。
9、onDestroyView()
销毁与 Fragment 有关的 视图 ,注意,此时未与 Activity 解绑,依然可以通过onCreateView 方法重新创建视图。
10、onDestroy()
销毁Fragment,通常按Back键退出或者Fragment被回收时调用此方法。
11、onDetach()
解除与Activity的绑定。
那么在一些操作场景下,它的生命周期是怎么走的呢?我们来看看。
调用 show() 和 hide() 方法时,
Fragment的正常生命周期方法并不会被执行,此时仅仅是 Fragment 的 View 被显示或者隐藏
调用 add() 方法
onAttach -> onCreate -> onCreateView -> onViewCreated -> onActivityCreated -> onStart -> onResume
调用 remove() 方法
onPause -> onStop -> onDestroyView -> onDestroy -> onDetach
调用 replace() 方法
其实就是执行上一个 Fragment 的 remove() 的生命周期 加上 下一个 Fragment 的 add() 的生命周期
调用 detach() 方法时,Fragment 的生命周期如下:
onPause() -> onStop() -> onDestroyView()
重新调用 attach() 方法后,生命周期如下:
onCreateView() -> onViewCreated() -> onActivityCreated() -> onStart() -> onResume()
由此我们也可以看得出来 add 和 attach 的区别:
使用 add() 方法添加 Fragment ,会触发 onAttach() 方法,
使用 attach() 方法添加 Fragment ,不会触发 onAttach() 方法。
当我们按下 home 键回到桌面时,生命周期如下:
onPause() —> onStop()
当我们从桌面回到 Fragment 时,生命周期如下:
onStart() —> onResume()
Fragment 通信问题
方式一:setFragmentResultListener
2020 年,谷歌发布了 Android 的新特性,其中就包括 Fragment 间通信的新方式,即
- 使用 setFragmentResult 发送数据
- 使用 setFragmentResultListener 接受数据
这种新方式有什么优势呢?包括一下几点:
- 在 Fragment 之间传递数据,它们不会持有对方的引用
- 生命周期处于活跃状态时才开始处理数据,避免 Fragment 处于不可预知状态的时,可能发生未知的问题
- 当生命周期处于 ON_DESTROY 时,自动移除监听
- 事件是粘性的,也就是说先发送消息,再创建监听,最新消息仍能回调给监听器
下面我们就来看看这种新的通信方式是如何使用的:
supportFragmentManager.setFragmentResult("request_key",
Bundle().also {
it.putInt("bundle_key", 1)
}
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
parentFragmentManager.setFragmentResultListener(
"request_key",
viewLifecycleOwner,
object : FragmentResultListener {
override fun onFragmentResult(requestKey: String, result: Bundle) {
}
})
}
注意,viewLifecycleOwner 只有在 Fragment 的 view 完成创建后才不会为 null,所以我们将接受数据的逻辑放在 Fragment 的 onViewCreated 方法中进行。
那么,setFragmentResultListener 中传递的 viewLifecycleOwner 有什么用呢?
我们知道,viewLifecycleOwner 是可以感知组件生命周期的,由此我们很容易联想到,接受数据的监听只有在 Fragment 处于活跃状态下才会得到更新的数据,另外,这个监听会在 Fragment 被销毁的时候一并销毁。
方式二:ViewModel + LiveData
这种方式也是谷歌官方推荐的用于 Fragment 之间或与宿主 Activity 之间通信的通信方式,
我们知道,只要 Activity 不被销毁,ViewModel 就会一直存在,并且即使我们进行横竖屏切换导致 Activity 重建了,也不会影响到ViewModel。
一个 Activity 可以包含多个 Fragment,并且这些 Fragment 是互相独立的,都属于同一个Activity。所以我们可以利用 ViewModel 和 LiveData,实现同一个 Activity 中的不同 Fragment 间的通信问题。
注意,仅限于同一个 Activity。不同 Activity 的 Fragment 之间的通信不能使用这种方式。
怎么使用呢,其实也很简单,在 MyFragment 的 onViewCreated 方法中,拿到宿主 Activity 的 ViewModel 的 LiveData ,并开始观察这个 LiveData 的数据更新。如下:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProvider(requireActivity())[ActivityViewModel::class.java]
viewModel.value.observe(viewLifecycleOwner) {
}
}
方式三:接口回调
接口回调的方式略微繁琐,但也是一种方式,这里举个例子说明一下(Fragment 向宿主 Activity 发送数据):
interface CallbackListener {
fun callback(msg: String)
}
class ParentControlLockActivity : AppCompatActivity() {
val listener = object : CallbackListener {
override fun callback(msg: String) {
}
}
}
class BlankFragment : DialogFragment(R.layout.fragment_blank) {
private var mListener: CallbackListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
mListener = (activity as ParentControlLockActivity).listener
mListener?.callback("Fragment发送消息")
}
}
其他方式
- setArgments
- Handler
- 广播
- EventBus
还有其他的通信方式,比较简单,这里就不一一举例了,感兴趣的大佬可以查阅其他资料。
DialogFragment
基于 Fragment,Android为我们提供了一种更好的 dialog 使用方式,即 DialogFragment ,它继承于 Fragmet 。DialogFragment 能实现 Dialog 的所有需求。
使用 DialogFragment ,可以更好的管理 dialog 的生命周期,想比于 AlertDialog,AlertDialog缺少生命周期的管理,另外外,在横竖屏切换等特殊情况下,使用 AlertDialog,可能会带来众多 bug 。
DialogFragment 的使用
首先创建一个 DialogFragment,代码如下:
private const val ARG_PARAM1 = "mTitle"
private const val ARG_PARAM2 = "mMessage"
class BlankFragment : DialogFragment(R.layout.fragment_blank) {
private var mTitle: String? = null
private var mMessage: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Theme_AppCompat_Dialog)
arguments?.let {
mTitle = it.getString(ARG_PARAM1)
mMessage = it.getString(ARG_PARAM2)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.title).text = mTitle
view.findViewById<TextView>(R.id.message).text = mMessage
}
companion object {
@JvmStatic
fun showDialog(fm: FragmentManager, param1: String, param2: String) {
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
show(fm, "BlankFragment")
}
}
}
}
当我们需要在 Activity 上展示这个 dialog 的时候,只需要调用如下代码即可:
BlankFragment.showDialog(supportFragmentManager, "title", "msg")
DialogFragment 除了以上的使用方式外,还有另一种使用方式,即重写 DialogFragment 的 onCreateDialog 方法,
private var mTitle: String? = null
private var mMessage: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Theme_AppCompat_Dialog)
arguments?.let {
mTitle = it.getString(ARG_PARAM1)
mMessage = it.getString(ARG_PARAM2)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).setTitle(mTitle).setMessage(mMessage).create()
}
companion object {
@JvmStatic
fun showDialog(fm: FragmentManager, param1: String, param2: String) {
BlankFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
show(fm, "BlankFragment")
}
}
}
}
在我们 show DialogFragment 的时候,是直接使用 Fragment.show() 方法来展示一个 Dialog 的,其内部实际上也是使用事务提交的方式,跟普通的 Fragment 使用是一样的。
public int show(@NonNull FragmentTransaction transaction, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
transaction.add(this, tag);
mViewDestroyed = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
|