- 簡(jiǎn)書(shū)
- CSDN
Fragment(四)常見(jiàn)問(wèn)題
博客對(duì)應(yīng)的Demo地址:GitHub、Gitee
通過(guò)這篇博客犁珠,我們能知道以下問(wèn)題:
-
Fragment
在不同情況下實(shí)現(xiàn)網(wǎng)絡(luò)延遲 -
Fragment
為什么一定要有無(wú)參構(gòu)造材诽? -
Fragment
與Activity
傳遞數(shù)據(jù)方式 - 嵌套
Fragment
時(shí)父Fragment
生命周期傳遞到子Fragment
中的方式
1. Fragment
在不同情況下實(shí)現(xiàn)網(wǎng)絡(luò)延遲
其實(shí)使用延遲加載主要目的是在頁(yè)面對(duì)用戶可見(jiàn)時(shí)在加載網(wǎng)絡(luò),避免資源浪費(fèi)寿烟,那么這個(gè)問(wèn)題就轉(zhuǎn)換成了 Fragment
在不同情況下怎樣判斷對(duì)用戶的可見(jiàn)性,這個(gè)問(wèn)題在前面的幾篇博客中都或多或少的提到了辛燥,這里直接做一個(gè)總結(jié):
add()
+show()/hide()
:生命周期方法不對(duì)筛武,多個(gè)添加的Fragment
一開(kāi)始就會(huì)會(huì)同時(shí)執(zhí)行到onResume()
,退出時(shí)又會(huì)同時(shí)執(zhí)行其他生命周期方法(onPause()
到onDetach()
)挎塌,所以不能直接通過(guò)生命周期方法處理徘六,而是需要通過(guò)onHiddenChanged(boolean hidden)
方法判斷。replace()
:“替換”榴都,這種方式會(huì)銷(xiāo)毀布局容器內(nèi)的已有Fragment
待锈,然后重新創(chuàng)建一個(gè)新的Fragment
嘴高,銷(xiāo)毀的Fragment
執(zhí)行onPause()
到onDetach()
回調(diào)方法竿音,新的Fragment
會(huì)執(zhí)行onAttach()
到onResume()
回調(diào)谍失,所以直接在onStart()
或onResume()
回調(diào)中處理就行了。-
ViewPager
:在AndroidX之前只有一種情況莹汤,在AndroidX中有兩種情況快鱼,在Adapter
構(gòu)造中增加了一個(gè)behavior
參數(shù)(取值:BEHAVIOR_SET_USER_VISIBLE_HINT
、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
)纲岭,非AndroidX就相當(dāng)于取值BEHAVIOR_SET_USER_VISIBLE_HINT
抹竹,所以兩種情況需要分別來(lái)看:BEHAVIOR_SET_USER_VISIBLE_HINT
:生命周期方法監(jiān)聽(tīng)不準(zhǔn)確,需要通過(guò)setUserVisibleHint()
方法來(lái)監(jiān)聽(tīng)止潮,當(dāng)方法傳入值為true
的時(shí)候窃判,說(shuō)明Fragment
可見(jiàn),為false
的時(shí)候說(shuō)明Fragment
被切走了喇闸。但是需要注意的是袄琳,這個(gè)方法不屬于生命周期方法询件,所以它可能在生命周期方法執(zhí)行之前就執(zhí)行了,也就是說(shuō)唆樊,有可能執(zhí)行這個(gè)方法的時(shí)候宛琅,Fragment
還沒(méi)有被添加到容器中,所以需要進(jìn)行判斷一下逗旁。BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
:生命周期方法是正常的嘿辟,只有正在顯示的Fragment
執(zhí)行到onResume()
方法,其他Fragment
只會(huì)執(zhí)行到onStart()
方法片效,并且當(dāng)Fragment
切換到顯示時(shí)執(zhí)行onResume()
方法红伦,切換到不顯示狀態(tài)時(shí)觸發(fā)onPause()
方法。
ViewPager2
:生命周期方法也是正常的淀衣,只有正在顯示的Fragment
執(zhí)行到onResume()
方法昙读,其他Fragment
只會(huì)執(zhí)行到onStart()
方法,并且當(dāng)Fragment
切換到顯示時(shí)執(zhí)行onResume()
方法膨桥,切換到不顯示狀態(tài)時(shí)觸發(fā)onPause()
方法箕戳。
2. Fragment
為什么一定要有無(wú)參構(gòu)造?
正常情況下国撵,我們?nèi)绻褂糜袇?gòu)造,自己在創(chuàng)建 Fragment
對(duì)象使用時(shí)玻墅,也是沒(méi)問(wèn)題的介牙,但是如果 FragmentActivity
銷(xiāo)毀自動(dòng)重建,恢復(fù)頁(yè)面狀態(tài)時(shí)澳厢,如果頁(yè)面包含 Fragment
环础,那么沒(méi)有無(wú)參構(gòu)造就會(huì)發(fā)送異常,我們從源碼里來(lái)看一下剩拢。
-
FragmentActivity
初始化方法public FragmentActivity() { super(); init(); } @ContentView public FragmentActivity(@LayoutRes int contentLayoutId) { super(contentLayoutId); init(); } private void init() { // 增加 Context 可用監(jiān)聽(tīng) addOnContextAvailableListener(new OnContextAvailableListener() { @Override public void onContextAvailable(@NonNull Context context) { mFragments.attachHost(null /*parent*/); Bundle savedInstanceState = getSavedStateRegistry() .consumeRestoredStateForKey(FRAGMENTS_TAG); // 如果 savedInstanceState 不為null线得,恢復(fù)頁(yè)面狀態(tài) if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); // 調(diào)用 FragmentController#restoreSaveState() 方法 mFragments.restoreSaveState(p); } } }); }
當(dāng)
FragmentActivity
恢復(fù)頁(yè)面狀態(tài)是,會(huì)調(diào)用FragmentController#restoreSaveState()
方法public void restoreSaveState(@Nullable Parcelable state) { mHost.mFragmentManager.restoreSaveState(state); }
調(diào)用
FragmentManager#restoreSaveState()
方法void restoreSaveState(@Nullable Parcelable state) { fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher, mFragmentStore, mHost.getContext().getClassLoader(), getFragmentFactory(), fs); } // 獲取 FragmentFactory 對(duì)象 public FragmentFactory getFragmentFactory() { if (mFragmentFactory != null) { return mFragmentFactory; } if (mParent != null) { return mParent.mFragmentManager.getFragmentFactory(); } return mHostFragmentFactory; } // FragmentFactory 對(duì)象初始化過(guò)程 private FragmentFactory mHostFragmentFactory = new FragmentFactory() { @Override public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { // 回調(diào)中調(diào)用 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); }
繼續(xù)查看
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) { } }
通過(guò)反射創(chuàng)建實(shí)例徐伐,并且調(diào)用的是無(wú)參構(gòu)造贯钩,所以為了避免異常產(chǎn)生,我們需要給
Fragment
定義無(wú)參構(gòu)造办素。
通過(guò)上面的分析角雷,我們知道了,雖然不給 Fragment
提供無(wú)參構(gòu)造性穿,正常情況下也能使用勺三,但是一旦 Fragment
容器異常重建,恢復(fù)狀態(tài)時(shí)需曾,那么就會(huì)拋出異常吗坚,導(dǎo)致崩潰祈远,所以我們一定需要給 Fragment
提供無(wú)參構(gòu)造,推薦用法商源,Fragment
不適用帶參構(gòu)造车份,參數(shù)是通過(guò) Fragment#setArguments()
方法傳遞。
3. Fragment
與 Activity
傳遞數(shù)據(jù)方式
在使用 Fragment
和 Activity
的過(guò)程當(dāng)中炊汹,不免需要進(jìn)行數(shù)據(jù)傳遞躬充,那么他們有哪些方式可以傳遞了。當(dāng)然這里所討論的方式讨便,不包括Android原生的廣播充甚、文件、內(nèi)容提供者霸褒、數(shù)據(jù)庫(kù)等形式伴找,也不包括第三方的 EventBus、RxBus 等全局通知形式废菱,而是僅內(nèi)存中他們相互傳遞的方式技矮。
Activity
向 Fragment
傳遞數(shù)據(jù)
首先我們看一下Activity
向 Fragment
傳遞數(shù)據(jù)的方式,一般有四種形式:構(gòu)造方法殊轴、Fragment#setArguments()
方法衰倦、自定義Fragment
實(shí)例方法和接口方式,我們來(lái)分別看一下
1. 構(gòu)造方法
這種方法不說(shuō)了旁理,一是比較簡(jiǎn)單樊零,二是不推薦使用帶參構(gòu)造創(chuàng)建Fragment
,原因在上面已經(jīng)說(shuō)過(guò)了孽文,就不再重復(fù)驻襟。-
2.
Fragment#setArguments()
這是Fragment
自帶的方法,通過(guò)Fragment
實(shí)例調(diào)用setArguments()
方法芋哭,可以傳遞一系列數(shù)據(jù)給Fragment
沉衣,Fragment
通過(guò)getArguments()
方法獲取。// 傳遞數(shù)據(jù) 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 } // 獲取數(shù)據(jù) arguments?.apply { var content = getString("content") var tag = getString("tag") var color = getInt("color") }
3. 自定義
Fragment
實(shí)例方法
這種方式和getArguments()
方法類似减牺,我們?cè)?Fragment
自定義方法豌习,然后再Activity
中獲取Fragment
對(duì)象,然后調(diào)用其方法烹植,傳遞數(shù)據(jù)給Fragment
斑鸦。-
4. 接口方式
定義一個(gè)接口,在需要傳遞數(shù)據(jù)的各個(gè)Fragment
中實(shí)現(xiàn)接口草雕,然后在注冊(cè)到宿主Activity
中巷屿,當(dāng)Activity
數(shù)據(jù)中發(fā)送改變時(shí),調(diào)用接口方法墩虹,將數(shù)據(jù)傳遞到實(shí)現(xiàn)接口的Fragment
中嘱巾。接口定義:
// 定義接口 interface ActivityDataChangeListener { fun onDataChange(message: String) }
Activity
中相關(guān)方法:// Activity中保存接口和增加注冊(cè)方法 private val listenerList = ArrayList<ActivityDataChangeListener>() /** * 注冊(cè)監(jiān)聽(tīng) */ open fun registerListener(dataChangeListener: ActivityDataChangeListener) { listenerList.add(dataChangeListener) } /** * 移除監(jiān)聽(tīng) */ open fun unRegisterListener(dataChangeListener: ActivityDataChangeListener) { listenerList.remove(dataChangeListener) } // Activity數(shù)據(jù)改變時(shí)憨琳,回調(diào)接口方法 titleView.titleContentView.setOnClickListener { for (dataChangeListener in listenerList) { dataChangeListener.onDataChange("currentItem: " + viewPager.currentItem) } }
Fragment
相關(guān)方法// 初始化監(jiān)聽(tīng) private var dataChangeListener = object : ActivityDataChangeListener { override fun onDataChange(message: String) { Logger.i("ActivityDataChangeListener: $message") } } // 頁(yè)面創(chuàng)建時(shí)注冊(cè)監(jiān)聽(tīng)到 Activity 中 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { (activity as VpFragmentActivity).registerListener(dataChangeListener) return super.onCreateView(inflater, container, savedInstanceState) } // 頁(yè)面銷(xiāo)毀時(shí)從 Activity 中移除監(jiān)聽(tīng) override fun onDestroyView() { super.onDestroyView() (activity as VpFragmentActivity).unRegisterListener(dataChangeListener) }
以上就是通過(guò)接口的方式,由
Activity
向Fragment
傳遞數(shù)據(jù)的主要代碼旬昭。
Fragment
向 Activity
傳遞數(shù)據(jù)
Fragment
向 Activity
傳遞數(shù)據(jù)的方式篙螟,一般有兩種形式:調(diào)用Activity
中的方法和接口方式,我們來(lái)分別看一下:
-
1. 調(diào)用
Activity
中的方法
這種方式就是直接在Activity
中定義方法问拘,然后在Fragment
中通過(guò)獲取getActivity()
然后強(qiáng)轉(zhuǎn)遍略,在調(diào)用方法。在
Activity
中定義方法open fun updateActivityData(message: String) { Logger.i("Activity data update: $message") }
Fragment
中獲取Activity
后強(qiáng)轉(zhuǎn)在調(diào)用方法骤坐,傳遞數(shù)據(jù)到Activity
绪杏。(activity as Vp2FragmentActivity).updateActivityData("新數(shù)據(jù)")
-
2. 接口方式:
定義一個(gè)接口,宿主Activity
實(shí)現(xiàn)接口纽绍,當(dāng)Fragment
數(shù)據(jù)中發(fā)送改變時(shí)蕾久,調(diào)用接口方法,將數(shù)據(jù)傳遞到實(shí)現(xiàn)接口的Activity
中拌夏。接口定義:
// 定義接口 interface FragmentDataChangeListener { fun onDataChange(message: String) }
Activity
中相關(guān)方法:// Activity中實(shí)現(xiàn)接口 FragmentDataChangeListener class Vp2FragmentActivity : BaseActivity(), FragmentDataChangeListener { // 重寫(xiě)方法 override fun onDataChange(message: String) { Logger.i("FragmentDataChangeListener: $message") } }
Fragment
相關(guān)方法private lateinit var dataChangeListener: FragmentDataChangeListener override fun onAttach(context: Context) { super.onAttach(context) // 將宿主Activity強(qiáng)轉(zhuǎn)成接口對(duì)象 dataChangeListener = activity as FragmentDataChangeListener } // 更新數(shù)據(jù)時(shí)僧著,調(diào)用接口方法,Activity中就會(huì)收到新的數(shù)據(jù) tvContent.setOnClickListener { dataChangeListener.onDataChange("Fragment 新數(shù)據(jù)") }
以上就是通過(guò)接口的方式障簿,由
Fragment
向Activity
傳遞數(shù)據(jù)的主要代碼盹愚。
擴(kuò)展: Fragment
與 Fragment
之間傳遞數(shù)據(jù)
如果兩個(gè) Fragment
是父子關(guān)系的話,那么與 Activity
和 Fragment
之間傳遞數(shù)據(jù)方式一樣站故;如果兩個(gè) Fragment
是兄弟關(guān)系的話(都是在同一個(gè) Activity
或 Fragment
中)杯拐,那么他們之間需要相互傳遞數(shù)據(jù)的話,就需要通過(guò)宿主進(jìn)行中轉(zhuǎn)了世蔗,先將數(shù)據(jù)傳遞向上給宿主 Activity
或 Fragment
中,然后在向下傳遞給另一個(gè) Fragment
朗兵,傳遞方式還是和上面一樣污淋。
4. 嵌套 Fragment
時(shí)父 Fragment
生命周期傳遞到子 Fragment
中的方式
正常情況下宿主 Fragment
在生命周期執(zhí)行的時(shí)候會(huì)相應(yīng)的分發(fā)到子 Fragment
中,但是 setUserVisibleHint()
和 onHiddenChanged()
卻沒(méi)有進(jìn)行相應(yīng)的回調(diào)余掖。試想一下寸爆,一個(gè) ViewPager
中有一個(gè) FragmentA
的tab,而 FragmentA
中有一個(gè)子FragmentB
盐欺,FragmentA
被滑走了赁豆,FragmentB
并不能接收到 setUserVisibleHint()
事件,onHiddenChange()
事件也是一樣的冗美,那么肯定是我們不希望看到的魔种,那么有什么辦法能夠避免這種問(wèn)題了。一般有兩種方式解決這個(gè)問(wèn)題:
- 宿主 Fragment 提供可見(jiàn)性的回調(diào)粉洼,子 Fragment 監(jiān)聽(tīng)改回調(diào)节预,有點(diǎn)類似于觀察者模式
- 宿主 Fragment 可見(jiàn)性變化的時(shí)候叶摄,主動(dòng)去遍歷所有的 子 Fragment,調(diào)用 子 Fragment 的相應(yīng)方法
1.宿主 Fragment 提供可見(jiàn)性的回調(diào)安拟,子 Fragment 監(jiān)聽(tīng)改回調(diào)
第一步蛤吓,先定義一個(gè)接口
interface OnFragmentVisibilityChangedListener {
fun onFragmentVisibilityChanged(visible: Boolean)
}
第二步,在 ParentFragment
中提供 addOnVisibilityChangedListener()
和 removeOnVisibilityChangedListener()
方法糠赦,這里需要注意的是会傲,我們需要用一個(gè) ArrayList
來(lái)保存所有的 listener
,因?yàn)橐粋€(gè)宿主 Fragment
可能有多個(gè)子 Fragment
拙泽。
當(dāng) Fragment
可見(jiàn)性變化的時(shí)候淌山,會(huì)遍歷 List
調(diào)用 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()
的時(shí)候奔滑,我們通過(guò) getParentFragment()
方法艾岂,拿到宿主 Fragment
,進(jìn)行監(jiān)聽(tīng)朋其。這樣王浴,當(dāng)宿主 Fragment
可見(jiàn)性變化的時(shí)候,子 Fragment
能感應(yīng)到梅猿。
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) {
// 可見(jiàn)性發(fā)送改變
}
override fun onDetach() {
super.onDetach()
if (parentFragment is ParentFragment) {
(parentFragment as ParentFragment).removeOnVisibilityChangedListener(this)
}
}
}
2. 宿主 Fragment 可見(jiàn)性變化的時(shí)候氓辣,主動(dòng)去遍歷所有的 子 Fragment
第一步,與第一種方式一樣袱蚓,先定義一個(gè)接口
interface OnFragmentVisibilityChangedListener {
fun onFragmentVisibilityChanged(visible: Boolean)
}
第二步钞啸,子 Fragment
實(shí)現(xiàn)接口
class ChildFragment : Fragment(), OnFragmentVisibilityChangedListener {
override fun onFragmentVisibilityChanged(visible: Boolean) {
// 可見(jiàn)性發(fā)送改變
}
}
第三步,宿主 Fragment 生命周期發(fā)生變化的時(shí)候喇潘,遍歷子 Fragment体斩,調(diào)用相應(yīng)的方法,通知生命周期發(fā)生變化
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)
}
}
}
}