简书
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)
}
}
}
}