- 简书
- CSDN
Fragment(四)常见问题
通过这篇博客,我们能知道以下问题:
-
Fragment
在不同情况下实现网络延迟 -
Fragment
为什么一定要有无参构造? -
Fragment
与Activity
传递数据方式 - 嵌套
Fragment
时父Fragment
生命周期传递到子Fragment
中的方式
1. Fragment
在不同情况下实现网络延迟
其实使用延迟加载主要目的是在页面对用户可见时在加载网络,避免资源浪费,那么这个问题就转换成了 Fragment
在不同情况下怎样判断对用户的可见性,这个问题在前面的几篇博客中都或多或少的提到了,这里直接做一个总结:
-
add()
+show()/hide()
:生命周期方法不对,多个添加的Fragment
一开始就会会同时执行到onResume()
,退出时又会同时执行其他生命周期方法(onPause()
到onDetach()
),所以不能直接通过生命周期方法处理,而是需要通过onHiddenChanged(boolean hidden)
方法判断。 -
replace()
:“替换”,这种方式会销毁布局容器内的已有Fragment
,然后重新创建一个新的Fragment
,销毁的Fragment
执行onPause()
到onDetach()
回调方法,新的Fragment
会执行onAttach()
到onResume()
回调,所以直接在onStart()
或onResume()
回调中处理就行了。 -
ViewPager
:在AndroidX之前只有一种情况,在AndroidX中有两种情况,在Adapter
构造中增加了一个behavior
参数(取值:BEHAVIOR_SET_USER_VISIBLE_HINT
、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
),非AndroidX就相当于取值BEHAVIOR_SET_USER_VISIBLE_HINT
,所以两种情况需要分别来看:-
BEHAVIOR_SET_USER_VISIBLE_HINT
:生命周期方法监听不准确,需要通过setUserVisibleHint()
方法来监听,当方法传入值为true
的时候,说明Fragment
可见,为false
的时候说明Fragment
被切走了。但是需要注意的是,这个方法不属于生命周期方法,所以它可能在生命周期方法执行之前就执行了,也就是说,有可能执行这个方法的时候,Fragment
还没有被添加到容器中,所以需要进行判断一下。 -
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
:生命周期方法是正常的,只有正在显示的Fragment
执行到onResume()
方法,其他Fragment
只会执行到onStart()
方法,并且当Fragment
切换到显示时执行onResume()
方法,切换到不显示状态时触发onPause()
方法。
-
-
ViewPager2
:生命周期方法也是正常的,只有正在显示的Fragment
执行到onResume()
方法,其他Fragment
只会执行到onStart()
方法,并且当Fragment
切换到显示时执行onResume()
方法,切换到不显示状态时触发onPause()
方法。
2. Fragment
为什么一定要有无参构造?
正常情况下,我们如果使用有参构造,自己在创建 Fragment
对象使用时,也是没问题的,但是如果 FragmentActivity
销毁自动重建,恢复页面状态时,如果页面包含 Fragment
,那么没有无参构造就会发送异常,我们从源码里来看一下。
-
FragmentActivity
初始化方法public FragmentActivity() { super(); init(); } @ContentView public FragmentActivity(@LayoutRes int contentLayoutId) { super(contentLayoutId); init(); } private void init() { // 增加 Context 可用监听 addOnContextAvailableListener(new OnContextAvailableListener() { @Override public void onContextAvailable(@NonNull Context context) { mFragments.attachHost(null /*parent*/); Bundle savedInstanceState = getSavedStateRegistry() .consumeRestoredStateForKey(FRAGMENTS_TAG); // 如果 savedInstanceState 不为null,恢复页面状态 if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); // 调用 FragmentController#restoreSaveState() 方法 mFragments.restoreSaveState(p); } } }); }
当
FragmentActivity
恢复页面状态是,会调用FragmentController#restoreSaveState()
方法public void restoreSaveState(@Nullable Parcelable state) { mHost.mFragmentManager.restoreSaveState(state); }
调用
FragmentManager#restoreSaveState()
方法void restoreSaveState(@Nullable Parcelable state) { fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher, mFragmentStore, mHost.getContext().getClassLoader(), getFragmentFactory(), fs); } // 获取 FragmentFactory 对象 public FragmentFactory getFragmentFactory() { if (mFragmentFactory != null) { return mFragmentFactory; } if (mParent != null) { return mParent.mFragmentManager.getFragmentFactory(); } return mHostFragmentFactory; } // FragmentFactory 对象初始化过程 private FragmentFactory mHostFragmentFactory = new FragmentFactory() { @Override public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { // 回调中调用 FragmentContainer#instantiate() 方法 return getHost().instantiate(getHost().getContext(), className, null); } };
FragmentContainer#instantiate()
方法public Fragment instantiate(@NonNull Context context, @NonNull String className, @Nullable Bundle arguments) { return Fragment.instantiate(context, className, arguments); }
继续查看
Fragment#instantiate()
方法public static Fragment instantiate(@NonNull Context context, @NonNull String fname, @Nullable Bundle args) { try { Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass( context.getClassLoader(), fname); Fragment f = clazz.getConstructor().newInstance(); if (args != null) { args.setClassLoader(f.getClass().getClassLoader()); f.setArguments(args); } return f; } catch (Exception e) { } }
通过反射创建实例,并且调用的是无参构造,所以为了避免异常产生,我们需要给
Fragment
定义无参构造。
通过上面的分析,我们知道了,虽然不给 Fragment
提供无参构造,正常情况下也能使用,但是一旦 Fragment
容器异常重建,恢复状态时,那么就会抛出异常,导致崩溃,所以我们一定需要给 Fragment
提供无参构造,推荐用法,Fragment
不适用带参构造,参数是通过 Fragment#setArguments()
方法传递。
3. Fragment
与 Activity
传递数据方式
在使用 Fragment
和 Activity
的过程当中,不免需要进行数据传递,那么他们有哪些方式可以传递了。当然这里所讨论的方式,不包括Android原生的广播、文件、内容提供者、数据库等形式,也不包括第三方的 EventBus、RxBus 等全局通知形式,而是仅内存中他们相互传递的方式。
Activity
向 Fragment
传递数据
首先我们看一下Activity
向 Fragment
传递数据的方式,一般有四种形式:构造方法、Fragment#setArguments()
方法、自定义Fragment
实例方法和接口方式,我们来分别看一下
-
1. 构造方法
这种方法不说了,一是比较简单,二是不推荐使用带参构造创建Fragment
,原因在上面已经说过了,就不再重复。 -
2.
Fragment#setArguments()
这是Fragment
自带的方法,通过Fragment
实例调用setArguments()
方法,可以传递一系列数据给Fragment
,Fragment
通过getArguments()
方法获取。// 传递数据 fun newInstance(content: String, color: Int): Vp2Fragment { val arguments = Bundle() arguments.putString("content", content) arguments.putInt("color", color) arguments.putString("tag", content) val vpFragment = Vp2Fragment() vpFragment.arguments = arguments return vpFragment } // 获取数据 arguments?.apply { var content = getString("content") var tag = getString("tag") var color = getInt("color") }
-
3. 自定义
Fragment
实例方法
这种方式和getArguments()
方法类似,我们在Fragment
自定义方法,然后再Activity
中获取Fragment
对象,然后调用其方法,传递数据给Fragment
。 -
4. 接口方式
定义一个接口,在需要传递数据的各个Fragment
中实现接口,然后在注册到宿主Activity
中,当Activity
数据中发送改变时,调用接口方法,将数据传递到实现接口的Fragment
中。接口定义:
// 定义接口 interface ActivityDataChangeListener { fun onDataChange(message: String) }
Activity
中相关方法:// Activity中保存接口和增加注册方法 private val listenerList = ArrayList<ActivityDataChangeListener>() /** * 注册监听 */ open fun registerListener(dataChangeListener: ActivityDataChangeListener) { listenerList.add(dataChangeListener) } /** * 移除监听 */ open fun unRegisterListener(dataChangeListener: ActivityDataChangeListener) { listenerList.remove(dataChangeListener) } // Activity数据改变时,回调接口方法 titleView.titleContentView.setOnClickListener { for (dataChangeListener in listenerList) { dataChangeListener.onDataChange("currentItem: " + viewPager.currentItem) } }
Fragment
相关方法// 初始化监听 private var dataChangeListener = object : ActivityDataChangeListener { override fun onDataChange(message: String) { Logger.i("ActivityDataChangeListener: $message") } } // 页面创建时注册监听到 Activity 中 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { (activity as VpFragmentActivity).registerListener(dataChangeListener) return super.onCreateView(inflater, container, savedInstanceState) } // 页面销毁时从 Activity 中移除监听 override fun onDestroyView() { super.onDestroyView() (activity as VpFragmentActivity).unRegisterListener(dataChangeListener) }
以上就是通过接口的方式,由
Activity
向Fragment
传递数据的主要代码。
Fragment
向 Activity
传递数据
Fragment
向 Activity
传递数据的方式,一般有两种形式:调用Activity
中的方法和接口方式,我们来分别看一下:
-
1. 调用
Activity
中的方法
这种方式就是直接在Activity
中定义方法,然后在Fragment
中通过获取getActivity()
然后强转,在调用方法。在
Activity
中定义方法open fun updateActivityData(message: String) { Logger.i("Activity data update: $message") }
Fragment
中获取Activity
后强转在调用方法,传递数据到Activity
。(activity as Vp2FragmentActivity).updateActivityData("新数据")
-
2. 接口方式:
定义一个接口,宿主Activity
实现接口,当Fragment
数据中发送改变时,调用接口方法,将数据传递到实现接口的Activity
中。接口定义:
// 定义接口 interface FragmentDataChangeListener { fun onDataChange(message: String) }
Activity
中相关方法:// Activity中实现接口 FragmentDataChangeListener class Vp2FragmentActivity : BaseActivity(), FragmentDataChangeListener { // 重写方法 override fun onDataChange(message: String) { Logger.i("FragmentDataChangeListener: $message") } }
Fragment
相关方法private lateinit var dataChangeListener: FragmentDataChangeListener override fun onAttach(context: Context) { super.onAttach(context) // 将宿主Activity强转成接口对象 dataChangeListener = activity as FragmentDataChangeListener } // 更新数据时,调用接口方法,Activity中就会收到新的数据 tvContent.setOnClickListener { dataChangeListener.onDataChange("Fragment 新数据") }
以上就是通过接口的方式,由
Fragment
向Activity
传递数据的主要代码。
扩展: Fragment
与 Fragment
之间传递数据
如果两个 Fragment
是父子关系的话,那么与 Activity
和 Fragment
之间传递数据方式一样;如果两个 Fragment
是兄弟关系的话(都是在同一个 Activity
或 Fragment
中),那么他们之间需要相互传递数据的话,就需要通过宿主进行中转了,先将数据传递向上给宿主 Activity
或 Fragment
中,然后在向下传递给另一个 Fragment
,传递方式还是和上面一样。
4. 嵌套 Fragment
时父 Fragment
生命周期传递到子 Fragment
中的方式
正常情况下宿主 Fragment
在生命周期执行的时候会相应的分发到子 Fragment
中,但是 setUserVisibleHint()
和 onHiddenChanged()
却没有进行相应的回调。试想一下,一个 ViewPager
中有一个 FragmentA
的tab,而 FragmentA
中有一个子FragmentB
,FragmentA
被滑走了,FragmentB
并不能接收到 setUserVisibleHint()
事件,onHiddenChange()
事件也是一样的,那么肯定是我们不希望看到的,那么有什么办法能够避免这种问题了。一般有两种方式解决这个问题:
- 宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调,有点类似于观察者模式
- 宿主 Fragment 可见性变化的时候,主动去遍历所有的 子 Fragment,调用 子 Fragment 的相应方法
1.宿主 Fragment 提供可见性的回调,子 Fragment 监听改回调
第一步,先定义一个接口
interface OnFragmentVisibilityChangedListener {
fun onFragmentVisibilityChanged(visible: Boolean)
}
第二步,在 ParentFragment
中提供 addOnVisibilityChangedListener()
和 removeOnVisibilityChangedListener()
方法,这里需要注意的是,我们需要用一个 ArrayList
来保存所有的 listener
,因为一个宿主 Fragment
可能有多个子 Fragment
。
当 Fragment
可见性变化的时候,会遍历 List
调用 OnFragmentVisibilityChangedListener
的 onFragmentVisibilityChanged()
方法
class ParentFragment : Fragment() {
private val listeners = ArrayList<OnFragmentVisibilityChangedListener>()
fun addOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
listener?.apply {
listeners.add(this)
}
}
fun removeOnVisibilityChangedListener(listener: OnFragmentVisibilityChangedListener?) {
listener?.apply {
listeners.remove(this)
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
for (listener in listeners) {
listener.onFragmentVisibilityChanged(!hidden)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
for (listener in listeners) {
listener.onFragmentVisibilityChanged(isVisibleToUser)
}
}
}
第三步,在 Fragment#attach()
的时候,我们通过 getParentFragment()
方法,拿到宿主 Fragment
,进行监听。这样,当宿主 Fragment
可见性变化的时候,子 Fragment
能感应到。
class ChildFragment : Fragment(), OnFragmentVisibilityChangedListener {
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is ParentFragment) {
(parentFragment as ParentFragment).addOnVisibilityChangedListener(this)
}
}
override fun onFragmentVisibilityChanged(visible: Boolean) {
// 可见性发送改变
}
override fun onDetach() {
super.onDetach()
if (parentFragment is ParentFragment) {
(parentFragment as ParentFragment).removeOnVisibilityChangedListener(this)
}
}
}
2. 宿主 Fragment 可见性变化的时候,主动去遍历所有的 子 Fragment
第一步,与第一种方式一样,先定义一个接口
interface OnFragmentVisibilityChangedListener {
fun onFragmentVisibilityChanged(visible: Boolean)
}
第二步,子 Fragment
实现接口
class ChildFragment : Fragment(), OnFragmentVisibilityChangedListener {
override fun onFragmentVisibilityChanged(visible: Boolean) {
// 可见性发送改变
}
}
第三步,宿主 Fragment 生命周期发生变化的时候,遍历子 Fragment,调用相应的方法,通知生命周期发生变化
class ParentFragment : Fragment() {
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
onFragmentVisibilityChanged(!hidden)
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
onFragmentVisibilityChanged(isVisibleToUser)
}
private fun onFragmentVisibilityChanged(visible: Boolean) {
var fragments = childFragmentManager.fragments
if (fragments.isEmpty()) return
for (fragment in fragments) {
if (fragment is OnFragmentVisibilityChangedListener) {
fragment.onFragmentVisibilityChanged(visible)
}
}
}
}
网友评论