close
安卓進階漲薪訓練營,讓一部分人先進大廠

大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術&職場瓶頸,從而度過難關,進入心儀的公司。

詳情見文章:沒錯!皇叔開了個訓練營

引言

Fragment 誕生之初就被定義為一個小型 Activity,因此它代理了 Activity 的許多能力(例如 startActivityForResult 等),職責不夠單一。隨着 Jetpack 各種新組件的出現,Fragment 的很多職責被有效地進行了分擔,其本身也可以更好地聚焦在對 UI 的劃分和管理上面,早設計的一些 API 也可以退出歷史舞台了。本文就盤點一下 Fragment 那些被廢棄的 API。

本文的介紹基於 Fragment 版本 1.4.0

1. instantiate

以前, Fragment 的構造函數不允許攜帶參數,因為某些場景中 Fragment 會由系統自動創建,例如基於 XML 創建 Fragment、Activity 被殺死後的恢復重建等等。此時,系統通過調用instantiate來創建 Fragment,instantiate 通過反射調用 Fragment 無參的構造函數。

現在 Fragment 的構造函數允許攜帶參數了,我們可以通過自定義FragmentFactory,調用 Fragment 的任意構造函數,而系統通過調用 FragmentFactory 來創建 Fragment。

我們可以自定義 FragmentFactory,並重寫它的 instantiate 方法來創建 Fragment:

classMyFragmentFactory(privatevalarg:Any):FragmentFactory(){overridefuninstantiate(classLoader:ClassLoader,className:String):Fragment{valclazz=loadFragmentClass(classLoader,className)if(clazz==MyFragment::class.java){returnMyFragment(arg)}returnsuper.instantiate(classLoader,className)}}

我們將 FragmentFactory 設置給 FragmentManger,之後系統就可以在各種場景中使用工廠創建 Fragment 了。

//ActivityoverridefunonCreate(savedInstanceState:Bundle?){supportFragmentManager.fragmentFactory=myFragmentFactorysuper.onCreate(savedInstanceState)}

注意 FragmentFactory 的設置必須在 super.onCreate 之前,因為當 Activity 進入重建路徑時,會在 super.onCreate 中使用到它。

關於 FragmentFactory 的更多介紹,請參考 FragmentFactory:構建Fragment的好幫手

2. onActivityCreated

Fragment 早期設計中與 Activity 耦合較多,例如在生命周期方面上除了代理了 Activity 標準生命周期回調以外,還增加了onActivityCreated用來觀察與 Activity 的綁定關係,onActivityCreated 被認為是onStart之前最後一個階段,此時 Fragment 的 View Hierarchy 已經與 Activity 綁定,因此常用來在這裡完成一些基於 View 的初始化工作。

現在,官方正在逐漸去掉 Fragment 與 Activity 之間的耦合,一個更加獨立的 Fragment 更利於復用和測試,因此onActivityCreated被廢除,取而代之的是在onViewCreated中處理與 View 相關的初始化邏輯,與 View 無關的初始化可以前置到onCreate。但要注意 onViewCreated 回調的時間點,Fragment 的 View 還沒加入 Activity View 的 Hierarchy。

如果我們實在需要獲得 Activity 的 onCreate 事件通知,可以通過在onAttach(Context)中通過LifecycleObserver來獲取

overridefunonAttach(context:Context){super.onAttach(context)requireActivity().lifecycle.addObserver(object:DefaultLifecycleObserver{overridefunonCreate(owner:LifecycleOwner){owner.lifecycle.removeObserver(this)//...}})}

onAttach(Context) 是 API23 之後新增的 API,前身是onAttach(Activity),它也是為了去掉與 Activity 的耦合而被廢棄和取代。

3. setRetainInstance

當系統發生橫豎屏旋轉等 ConfigurationChanged 時,伴隨 Activity 的重新 onCreate,Fragment 也會重新創建。setRetainInstance(true)可以保持 ConfigurationChanged 之後的 Fragment 實例不變。因為有這個特性,以前我們經常會藉助 setRetainInstance 來保存 Fragment 甚至 Activity 的狀態。

但是使用 setRetainInstance 保存狀態存在隱患,如果 Fragment 持有了對 Activity View 的引用則會造成泄露或者異常,所以我們僅保存與 View 無關的狀態即可,不應該保存整個 Fragment 實例,所以 setRetainInstance/getRetainInstance 被廢棄,取而代之的是推薦使用 ViewModel 保存狀態。

對於 ViewModel 的基操想必大家都很熟悉就不贅述了。這裡只提醒一點,既然 ViewModel 可以在 ConfigurationChanged 之後保持狀態,那麼 ViewModel 的初始化只需進行一次即可。不少人會像下面這樣初始化 ViewModel

classDetailTaskFragment:Fragment(R.layout.fragment_detailed_task){privatevalviewModel:DetailTaskViewModelbyviewModels()overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)//訂閱ViewModelviewMode.uiState.observe(viewLifecycleOwner){//updateui}//請求數據viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))}}

在 onViewCreated 中使用fetchTaskData請求數據,當橫豎屏旋轉造成 Fragment 重建時,雖然我們可以從 ViewModel 中獲取最新數據,但是仍然會執行一次多餘的 fetchTaskData 。因此更合理的 ViewModel 初始化時機應該是在其內部的init中進行,代碼如下:

classTasksViewModel:ViewModel(){privateval_tasks=MutableLiveData<List<Task>>()valtasks:LiveData<List<Task>>=_uiStateinit{viewModelScope.launch{_tasks.value=withContext(Dispatchers.IO){TasksRepository.fetchTasks()}}}}

關於 ViewModel 初始化時機的相關內容,可以參考:Jetpack MVVM七宗罪 之三 :在 onViewCreated 中請求數據

4. setUserVisibleHint

Fragment 經常配合 ViewPager 使用以滿足多 Tab 頁場景的需求。默認情況下屏幕外部的 Fragment 會跟隨顯示中的 Fragment 一同被加載,這會影響初始頁面的顯示速度。setUserVisibleHint是以前我們常用的「懶加載」實現方案:當 ViewPager 中的 Fragment 進/出屏幕時,FragmentPagerAdapter會對其調用 setUserVisibleHint,傳入 true/false,通知其是否可見:

@OverridepublicvoidsetUserVisibleHint(booleanisVisibleToUser){super.setUserVisibleHint(isVisibleToUser);if(isVisibleToUser){onVisible();//自定義回調:進入屏幕}else{onInVisible();//離開屏幕}}

如上,通過重寫 setUserVisibleHint 我們可以在onVisible/onInVisible中獲知 Fragment 顯示的時機,便於實現懶加載。但是這種做法有缺陷,首先,你需要為 Fragment 增加基類來定義 onVisible/onInvisible,其次,新增的這兩個方法跟原生的生命周期回調交織在一起,增加了代碼複雜度和出錯的概率。幸好現在我們有了新的「懶加載」解決方案:FragmentTransaction#setMaxLifecycle:setMaxLifecycle 可以將屏幕外尚未顯示的 Fragment 的最大的生命周期的狀態限制在Started

當 Fragment 真正進入屏幕後再推進到Resumed,此時onResume才會響應。藉助 setMaxLifecycle 我們僅依靠原生回調即可實現懶加載,而且還避免了額外基類的引入。

如果你使用的是 ViewPager2,其對應的FragmentStateAdapter已經默認支持了 setMaxLifecycle 。對於傳統的 ViewPager,啟動 setMaxLifecycle 的方法也很簡單,FragmentPagerAdapter的構造方法新增了一個behavior參數, 只要在此處傳值為 FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT即可,在instantiateItem方法中,會根據 behavior 為創建的 Fragment 設置 setMaxLifecycle。

//FragmentPagerAdpater.java@OverridepublicObjectinstantiateItem(@NonNullViewGroupcontainer,intposition){...if(fragment!=mCurrentPrimaryItem){fragment.setMenuVisibility(false);//mBehaviour為1的時候走新邏輯if(mBehavior==BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){//初始化item時將其生命周期限制為STARTEDmCurTransaction.setMaxLifecycle(fragment,Lifecycle.State.STARTED);}else{//兼容舊版邏輯fragment.setUserVisibleHint(false);}}returnfragment;}

關於 setMaxLifecycle 背後的工作原理,可以參考:一道面試題:ViewPager中的Fragment如何實現懶加載?

5. onActivityResult

以前,我們在 Fragment 可以通過startActivityForResult/onActivityResult啟動 Activity 並獲取返回的結果,這本質是調用了 Activity 的同名方法。隨着Activity Result API的啟用,startActivityForResult/onActivityResult 已經在 Activity 以及 Fragment 中被廢棄。相對於 onActivityResult 的結果返回方式,Activity Result API 避免了對 requestCode 的依賴,以更加直觀的方式獲得 Activity 返回結果。

基本使用步驟如下圖:

首先,我們創建一個ActivityResultContract,這裡定義了跨 Activity 通信的輸入輸出協議,系統預置了一系列ActivityResultContracts.XXXX可直接使用。然後,我們使用registerForActivityResult註冊我們的 Contract 和對應的 Callback,Callback 中我們可以獲取 Activity 的返回結果。代碼如上

vallauncher:ActivityResultLauncher=registerForActivityResult(//使用預置的 Contract:StartActivityForResultActivityResultContracts.StartActivityForResult()){activityResult->//獲取Activity返回的ActivityResultLog.d("TargetActivity",activityResult.toString())//D/TargetActivity:ActivityResult{resultCode=RESULT_OK,data=Intent{(hasextras)}}}

registerForActivityResult 會返回一個ActivityResultLauncher句柄,我們使用它啟動 Activity,如下:

valintent=Intent(this,TargetActivity::class.java)launcher.launch(intent)

最後我們在目標 Activity 中調用setResult返回結果即可:

//TargetActivity.ktclassTargetActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setResult(Activity.RESULT_OK,Intent().putExtra("my-data","data"))finish()}}

關於 Activity Result API 的更多底層原理可以參考丟掉 onActivityResult,擁抱 Result API

6. requestPermissions

requestPermissions/onRequestPermissionsResult底層也是基於 startActivityForResult/onActivityResult 實現的,因此同樣被廢棄了,升級為 Result API 的方式。

ActivityResultContracts 預置了申請權限相關的 Contract:

request_permission.setOnClickListener{requestPermission.launch(permission.BLUETOOTH)}request_multiple_permission.setOnClickListener{requestMultiplePermissions.launch(arrayOf(permission.BLUETOOTH,permission.NFC,permission.ACCESS_FINE_LOCATION))}//申請單一權限privatevalrequestPermission=registerForActivityResult(ActivityResultContracts.RequestPermission()){isGranted->//Dosomethingifpermissiongrantedif(isGranted)toast("Permissionisgranted")elsetoast("Permissionisdenied")}//一次申請多權限privatevalrequestMultiplePermissions=registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()){permissions:Map<String,Boolean>->//Dosomethingifsomepermissionsgrantedordeniedpermissions.entries.forEach{//Docheckinghere}}7. setTargetFragment

setTargetFragment/getTargetFragment原本用於 Fragment 之間的通信,例如從 FragmentA 跳轉到 FragmentB ,在 B 中發送結果返回給 A:

//向FragmentB設置targetFragmentFragmentBfragment=newFragmentB();fragment.setTargetFragment(FragmentA.this,AppConstant.REQ_CODE_SECOND_FRAGMENT);//切換至FragmentBtransaction.replace(R.id.fragment_container,fragment).commit();//FragmentB中獲取FragmentA並進行回調Fragmentfragment=getTargetFragment();fragment.onActivityResult(AppConstant.REQ_CODE_SECOND_FRAGMENT,Activity.RESULT_OK,inte

如上,代碼非常簡單,但是這樣的通信無法感應生命周期,即使 FragmentA 處於後台也會在onActivityResult響應回調。目前 TargetFragment 相關 API 已經被廢棄,取而代之的是更為合理的Fragment Result API。

假設需要在 FragmentA 監聽 FragmentB 返回的數據,首先在 FragmentA 設置監聽

overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)//setFragmentResultListener是fragment-ktx提供的擴展函數setFragmentResultListener("requestKey"){requestKey,bundle->//監聽key為「requestKey」的結果,並通過bundle獲取valresult=bundle.getString("bundleKey")//...}}//setFragmentResultListener是Fragment的擴展函數,內部調用FragmentManger的同名方法publicfunFragment.setFragmentResultListener(requestKey:String,listener:((requestKey:String,bundle:Bundle)->Unit)){parentFragmentManager.setFragmentResultListener(requestKey,this,listener)}

當從 FragmentB 返回結果時:

valresult="result"setFragmentResult("requestKey",bundleOf("bundleKey"toresult))//setFragmentResult也是Fragment的擴展函數,其內部調用FragmentManger的同名方法publicfunFragment.setFragmentResult(requestKey:String,result:Bundle){parentFragmentManager.setFragmentResult(requestKey,result)}

上面的代碼可以用下圖表示:

FragmentA 通過Key向 FragmentManager 註冊ResultListener,FragmentB 返回 result 時, FM 通過 Key 將結果回調給FragmentA ,而且最重要的是 Result API 是生命周期可感知的,listener.onFragmentResult在Lifecycle.Event.ON_START的時候才調用,也就是說只有當 FragmentA 返回到前台時,才會收到結果。

關於 Fragment Result API 的更多介紹,可以參考:盤點一下 Fragment 間的幾種通信方式

最後

Fragment 是幫助我們組織和管理 UI 的重要組件,即使在 Compose 時代也具有使用價值,因此谷歌官方一直致力於對它的 API 的優化,希望它更加易用和便於測試。這些已廢棄的 API 在未來的版本中將會徹底刪除,所以如果你還在使用着他們,應該儘快予以替換。

官方也提供了工具幫助我們發現對於過期 API 的使用,Fragment-1.4.0 之後,我們可以通過全局設置嚴格模式策略,發現項目中的問題:

classMyApplication:Application(){overridefunonCreate(){super.onCreate()FragmentStrictMode.defaultPolicy=FragmentStrictMode.Policy.Builder().detectFragmentTagUsage()//setTargetFragment的使用.detectRetainInstanceUsage()//setRetainInstance的使用.detectSetUserVisibleHint()//setUserVisibleHint的使用.detectTargetFragmentUsage()//setTargetFragment的使用.apply{if(BuildConfig.DEBUG){//Debug模式下崩潰penaltyDeath()}else{//Release模式下上報penaltyListener{FirebaseCrashlytics.getInstance().recordException(it)}}}.build()}}

關於 FragmentStrictMode 的更多內容,請參考:https://developer.android.com/guide/fragments/debugging#strictmode



為了防止失聯,歡迎關注我防備的小號


微信改了推送機制,真愛請星標本公號👇

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()