在
Android Jetpack
組件中,fragment
作為視圖控制器之一占有很重要的位置。但由於其bug眾多,暗坑無數,以至於 Square 有這樣一篇博客:Advocating Against Android Fragments。github上的 Fragmentation 有着 9.4k 的star。而現在,
androidx fragment
穩定版已來到 1.2.2,讓我們總結一下fragment
有哪些常見問題以及有哪些使用fragment
的新姿勢
Fragment 常見的問題
-
getSupportFragmentManager , getParentFragmentManager 和 getChildFragmentManager
-
FragmentStateAdapter 和 FragmentPagerAdapter
-
add 和 replace
-
observe LiveData時傳入 this 還是 viewLifecycleOwner
-
使用 simpleName 作為 fragment 的 tag 有何風險?
-
在 BottomBarNavigation 和 drawer 中如何使用Fragment多次添加?
-
返回棧
getSupportFragmentManager , getParentFragmentManager和getChildFragmentManager
FragmentManager
是androidx.fragment.app
(已棄用的不考慮)下的抽象類,創建用於 添加,移除,替換fragment
的事務(transaction
)
首先要確認一件事,getSupportFragmentManager()
是 FragmentActivity
下的方法
getParentFragmentManager
和 getChildFragmentManager
是 androidx.fragment.app.Fragment
下的方法,
其中
androidx.fragment 1.2.0
后getFragmentManager
與requireFragmentManager
已棄用
明確了這件事,接下來的就很清晰了
getSupportFragmentManager
與activity
關聯,可以將其視為activity
的FragmentManager
getChildFragmentManager
與fragment
關聯,可以將其視為fragment
的FragmentManager
getParentFragmentManager
情況稍微復雜,正常情況返回的是該fragment
依附的activity
的FragmentManager
。如果該fragment是另一個fragment
的子fragment
,則返回的是其父fragment
的getChildFragmentManager
如果這么說還不明白的話,我們可以做一個實踐。
創建一個 activity
,一個父fragment
,一個子fragment
// activity
class MyActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.commit {
add<ParentFragment>(R.id.content)
}
Log.i("MyActivity", "supportFragmentManager $supportFragmentManager")
}
}
class ParentFragment : Fragment(R.layout.fragment_parent) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
childFragmentManager.commit {
add<ChildFragment>(R.id.content)
}
Log.i("ParentFragment", "parentFragmentManager $parentFragmentManager")
Log.i("ParentFragment", "childFragmentManager $childFragmentManager")
}
}
class ChildFragment : Fragment(R.layout.fragment_child) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("ChildFragment", "parentFragmentManager $parentFragmentManager")
Log.i("ChildFragment", "childFragmentManager $childFragmentManager")
}
}
//log
I/MyActivity: supportFragmentManager FragmentManager{825dcef in HostCallbacks{14a13fc}}}
I/ParentFragment: parentFragmentManager FragmentManager{825dcef in HostCallbacks{14a13fc}}}
I/ParentFragment: childFragmentManager FragmentManager{df5de83 in ParentFragment{7cdd800}}}
I/ChildFragment: parentFragmentManager FragmentManager{df5de83 in ParentFragment{7cdd800}}}
I/ChildFragment: childFragmentManager FragmentManager{aba9afb in ChildFragment{5cea718}}}
因此
-
在
activity
中使用ViewPager
,BottomSheetFragment
和DialogFragment
時,都應使用getSupportFragmentManager
-
在
fragment
中使用ViewPager
時應該使用getChildFragmentManager
錯誤的在 fragment
中使用 activity
的 FragmentManager
會引發內存泄露。 為什么呢?假如您的fragment中有一些依靠 ViewPager
管理的子 fragment
,並且所有這些 fragment
都在 activity
中,因為您使用的是activity
的FragmentManager
。 現在,如果關閉您的父fragment
,它將被關閉,但不會被銷毀,因為所有子fragment
都處於活動狀態,並且它們仍在內存中,從而導致泄漏。 它不僅會泄漏父fragment
,還會泄漏所有子fragment
,因為它們都無法從堆內存中清除。
FragmentStateAdapter 和 FragmentPagerAdapter
FragmentPagerAdapter
將整個 fragment
存儲在內存中,如果ViewPager
中使用了大量 fragment
,則可能導致內存開銷增加。 FragmentStatePagerAdapter
僅存儲片段的savedInstanceState
,並在失去焦點時銷毀所有 fragment
。
讓我們看看常見的兩個問題
1. 刷新ViewPager不生效
ViewPager
中的 fragment
是通過 activity
或 fragment
的 FragmentManager
管理的,FragmentManager
包含了viewpager
的所有fragment
的實例
因此,當ViewPager
沒有刷新時,它只是FragmentManager
仍保留的舊 fragment
實例。 您需要找出為什么FragmentManger
持有fragment
實例的原因。
2. 在Viewpager中訪問當前fragment
這也是我們遇到的一個非常普遍的問題。 如果遇到這種情況,我們一般在 adapter
內部創建 fragment
的數組列表,或者嘗試使用某些標簽訪問fragment
。 不過還有另一種選擇。 FragmentStateAdapter
和FragmentPagerAdapter
都提供方法setPrimaryItem
。 可以用來設置當前fragment
,如下所示:
var fragment: ChildFragment? = null
override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
if (getChildFragment() != any)
fragment = any as ChildFragment
super.setPrimaryItem(container, position, any)
}
fun getChildFragment(): ChildFragment? = fragment
//use
mAapter.getChildFragment()
add 和 replace 如何選擇?
在我們的activity
中,我們有一個容器,其中裝有fragment
。
add
只會將一個fragment
添加到容器中。 假設您將FragmentA
和FragmentB
添加到容器中。 容器將具有FragmentA
和FragmentB
,如果容器是FrameLayout
,則將fragment
一個添加在另一個之上。
replace
將簡單地替換容器頂部的一個fragment
,因此,如果我創建了 FragmentC
並 replace
頂部的 FragmentB
,則FragmentB
將被從容器中刪除(執行onDestroy
,除非您調用addToBackStack
,僅執行onDestroyView
),而FragmentC
將位於頂部。
那么如何選擇呢? replace
刪除現有fragment
並添加一個新fragment
。 這意味着當您按下返回按鈕時,將創建被替換的fragment
,並調用其onCreateView
。 另一方面,add
保留現有fragment
,並添加一個新fragment
,這意味着現有fragment
將處於活動狀態,並且它們不會處於 “paused” 狀態。 因此,按下返回按鈕時,現有fragment
(添加新fragment
之前的fragment
)不會調用onCreateView
。 就fragment
的生命周期事件而言,在replace
的情況下將調用onPause
,onResume
,onCreateView
和其他生命周期事件,在add
的情況下則不會。
如果不需要重新訪問當前fragment
並且不再需要當前fragment
,請使用replace
。 另外,如果您的應用有內存限制,請考慮使用replace
。
observe LiveData時傳入 this 還是 viewLifecycleOwner
androidx fragment 1.2.0
起,添加了新的 Lint 檢查,以確保您在從 onCreateView()
、onViewCreated()
或 onActivityCreated()
觀察 LiveData
時使用 getViewLifecycleOwner()
使用 simpleName 作為 fragment 的 tag 有何風險?
一般情況下我們會使用calss的simpleName
作為fragment
的tag
supportFragmentManager.commit {
replace(R.id.content,MyFragment.newInstance("Fragment"),
MyFragment::class.java.simpleName)
addToBackStack(null)
}
這樣做不會出現什么問題,但是...
val fragment = supportFragmentManager.findFragmentByTag(tag)
這樣獲取到的fragment可能不是想要的結果。
為什么呢?
加入有兩個 fragment,經過混淆,它們變成
com.mypackage.FragmentA → com.mypackage.c.a
com.mypackage.FragmentB → com.mypackage.c.a.a
上面是混淆了 full name,如果是simpleName 呢?
com.mypackage.FragmentA → a
com.mypackage.FragmentB → a
WTF!
所以在設置tag時盡量用全名或者常量
在 BottomBarNavigation 和 drawer 中如何使用Fragment多次添加?
當我們使用BottomBarNavigation
和 NavigationDrawer
時,通常會看到諸如fragment
重建或多次添加相同fragment
之類的問題。
在這種情況下,您可以使用show / hide
而不是 add
或 replace
。
返回棧
如果您想在fragment
的一系列跳轉中按返回鍵返回上一個fragment
,應該在commit
transaction
之前調用addToBackStack
方法
//使用該擴展 androidx.fragment:fragment-ktx:1.2.0 以上
parentFragmentManager.commit {
addToBackStack(null)
add<SecondFragment>(R.id.content)
}
Fragment 的使用新姿勢
-
fragment-ktx 有哪些好用的擴展函數
-
fragment 之間和與 activity 通信
-
使用 FragmentContainerView 作為 fragment 容器
-
FragmentFactory 的使用
-
Fragment 返回鍵攔截
-
Fragment 使用 ViewBinding
-
Fragment 使用 ViewPager2
-
不需要重寫 onCreateView 了?
-
使用require_()方法
fragment-ktx 有哪些好用的擴展函數
1. FragmentManagerKt
//before
supportFragmentManager
.beginTransaction()
.add(R.id.content,Fragment1())
.commit()
//after
supportFragmentManager.commit {
add<Fragment1>(R.id.content)
}
2. FragmentViewModelLazyKt
//before
//共享范圍activity
val mViewMode1l = ViewModelProvider(requireActivity()).get(UpdateAppViewModel::class.java)
//共享范圍fragment 內部
val mViewMode1l = ViewModelProvider(this).get(UpdateAppViewModel::class.java)
//after
//共享范圍activity
private val mViewModel by activityViewModels<MyViewModel>()
//共享范圍fragment 內部
private val mViewModel by viewModel<MyViewModel>()
注意:ViewModelProviders.of(this).get(MyViewModel.class); 的方式已棄用
lifecycle-extensions
依賴包已棄用
fragment 之間和與 activity 通信
fragment 和 fragment之間,fragment 和 activity 之間的通信有很多方法,android jetpack 推薦我們使用 ViewModel + LiveData 處理
同一個activity 內的 fragment 之間通信,可以使用作用范圍為activity的ViewModel,activity與 fragment通信同理。詳情可移步 Android官方應用架構指南
使用 FragmentContainerView 作為 fragment 容器
過去我們使用 FrameLayout
作為 Fragment
的容器,在 AndroidX Fragment 1.2.0
后,可以使用 FragmentContainerView
代替 FrameLayout
。
它修復了一些動畫 z軸索引順序問題和窗口插入調度,這意味着兩個fragment
之間的退出和進入過渡不會互相重疊。使用FragmentContainerView
將先開啟退出動畫然后才是進入動畫。
FragmentContainerView
是專門為 fragment設計的自定義View,它繼承自 FrameLayout
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>
FragmentFactory 的使用
過去,我們只能使用其默認的空構造函數實例化Fragment實例。 這是因為在某些情況下,例如配置更改和應用程序的流程重新創建,系統需要重新初始化。 如果不是默認的構造方法,系統將不知道如何重新初始化Fragment實例。
創建FragmentFactory來解決此限制。 通過向其提供實例化Fragment所需的必要參數/依賴關系,它可以幫助系統創建Fragment實例。
過去我們實例化fragment並傳遞參數會使用類似下面的代碼
class MyFragment : Fragment() {
private lateinit var arg: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.getString(ARG) ?: ""
}
companion object {
fun newInstance(arg: String) =
MyFragment().apply {
arguments = Bundle().apply {
putString(ARG, arg)
}
}
}
}
//use
val fragment = MyFragment.newInstance("my argument")
如果您的Fragment有一個非空的構造函數,則需要創建一個FragmentFactory來處理它的初始化。
class MyFragmentFactory(private val arg: String) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
if (className == MyFragment::class.java.name) {
return MyFragment(arg)
}
return super.instantiate(classLoader, className)
}
}
fragment
由FragmentManager
管理,因此很自然,FragmentFactory
需要添加到FragmentManager
才能使用。
那么什么時候把FragmentFactory
添加到FragmentManager
呢?
父類調用 Activity#onCreate()
和 Fragment#onCreate()
之前
class HostActivity : AppCompatActivity() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}
class ParentFragment : Fragment() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())
override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}
如果您的Fragment
具有默認的空構造函數,則無需使用FragmentFactory
。 但是,如果您的Fragment
在其構造函數中接受參數,則必須使用FragmentFactory
,否則將拋出Fragment.InstantiationException
,因為將使用的默認FragmentFactory
將不知道如何實例化Fragment
的實例。
Fragment 返回鍵攔截
有時候,您需要阻止用戶返回上一級。 在這種情況下,您需要在 Activity
中重寫 onBackPressed()
方法。 但是,當您使用 Fragment
時,沒有直接的方法來攔截返回。 在 Fragment
類中沒有可用的 onBackPressed()
方法,這是為了防止同時存在多個 Fragment
時發生意外行為。
但是,從 AndroidX
Activity 1.0.0
開始,您可以使用 OnBackPressedDispatcher
在您可以訪問該 Activity
的代碼的任何位置(例如,在 Fragment
中)注冊 OnBackPressedCallback
。
class MyFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Do something
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
}
Fragment 使用 ViewBinding
Android Studio 3.6.0
后提供了 ViewBindind
的支持,完整使用流程參見 [譯]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
_binding = null
}
}
Fragment 使用 ViewPager2
ViewPager
使用了三個adapter
的抽象類,而ViewPager2
中只有兩個
- ViewPager 中使用
PagerAdaper
,ViewPager2 中使用Recyclerview.Adapter
- ViewPager 中使用
FragmentPagerAdapter
,ViewPager2中使用FragmentStateAdapter
- ViewPager 中使用
FragmentStatePagerAdapter
,ViewPager2中使用FragmentStateAdapter
// A simple ViewPager adapter class for paging through fragments
class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
override fun getCount(): Int = NUM_PAGES
override fun getItem(position: Int): Fragment = ScreenSlidePageFragment()
}
// An equivalent ViewPager2 adapter class
class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = NUM_PAGES
override fun createFragment(position: Int): Fragment = ScreenSlidePageFragment()
}
使用 TabLayout
的變化,TabLayout
已從ViewPager2
中解耦,如果使用TabLayout
,需要引入依賴
implementation "com.google.android.material:material:1.1.0"
對於ViewPager2
,TabLayout
布局應與ViewPager2
在同一級別
<!-- A ViewPager element with a TabLayout -->
<androidx.viewpager.widget.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.viewpager.widget.ViewPager>
<!-- A ViewPager2 element with a TabLayout -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
使用ViewPager
時,TabLayout
與ViewPager
聯動需要調用 setupWithViewPager
,並重寫getPageTitle
方法,而ViewPager2
改為使用TabLayoutMediator
對象
// Integrating TabLayout with ViewPager
class CollectionDemoFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tabLayout = view.findViewById(R.id.tab_layout)
tabLayout.setupWithViewPager(viewPager)
}
...
}
class DemoCollectionPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
override fun getCount(): Int = 4
override fun getPageTitle(position: Int): CharSequence {
return "OBJECT ${(position + 1)}"
}
...
}
// Integrating TabLayout with ViewPager2
class CollectionDemoFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tabLayout = view.findViewById(R.id.tab_layout)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = "OBJECT ${(position + 1)}"
}.attach()
}
...
}
不需要重寫 onCreateView 了?
androidx fragment 1.1.0
后,您可以使用將 layoutId
作為參數的構造函數,這樣就無需重寫 onCreateView
方法了
class MyActivity : AppCompatActivity(R.layout.my_activity)
class MyFragmentActivity: FragmentActivity(R.layout.my_fragment_activity)
class MyFragment : Fragment(R.layout.my_fragment)
使用require_()方法
androidx fragment 1.2.2
起,新增了一項lint檢查,fragment
建議使用關聯的require_()
方法獲取更多描述性錯誤消息,而不是使用checkNotNull(get_())
,requireNonNull(get_())
或get()!
適用於所有包含 get 和 require Fragment API
例如:使用 requireActivity()
替代 getActivity()
Android熱修復框架、插件化框架、組件化框架、圖片加載框架、網絡訪問框架、RxJava響應式編程框架、IOC依賴注入框架、最近架構組件Jetpack等等Android開源框架。系統教程知識筆記已整理成PDF電子書見【GitHub】
文末
其實成為一名優秀的程序員並不難。
但是怎樣才能成為一名優秀的程序員?
我認為最大的阻礙在於:廣度與深度難以兼顧。
計算機專業基礎課,如OS,數據庫,網絡,算法等,抽象且難以理解,大學時不學,以后就很難拾起來。
既強調動手,又強調抽象,二者缺一不可。但善於思考的人,往往喜歡謀定而后動;善於行動的人,往往沒功夫回顧思考。
對於要先理解才動手的人,是種折磨。往往做了一兩年,才突然理解某個概念。
對於初學者,難以區分學的知識,還是配置。
雜訊太多,不知道學什么。
總得來說,編程里最簡單的地方往往價值不高,困難的地方這次避開了,下次還是要理解,逃也逃不掉。