你真的會用Fragment嗎?Fragment常見問題以及androidx下Fragment的使用新姿勢


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

FragmentManagerandroidx.fragment.app(已棄用的不考慮)下的抽象類,創建用於 添加,移除,替換 fragment 的事務(transaction

首先要確認一件事,getSupportFragmentManager()FragmentActivity下的方法

getParentFragmentManagergetChildFragmentManagerandroidx.fragment.app.Fragment 下的方法,

其中 androidx.fragment 1.2.0getFragmentManagerrequireFragmentManager 已棄用

明確了這件事,接下來的就很清晰了

  • getSupportFragmentManageractivity關聯,可以將其視為 activityFragmentManager
  • getChildFragmentManagerfragment關聯,可以將其視為fragmentFragmentManager
  • getParentFragmentManager情況稍微復雜,正常情況返回的是該fragment 依附的activityFragmentManager。如果該fragment是另一個fragment 的子 fragment,則返回的是其父fragmentgetChildFragmentManager

如果這么說還不明白的話,我們可以做一個實踐。

創建一個 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 中使用 ViewPagerBottomSheetFragmentDialogFragment 時,都應使用 getSupportFragmentManager

  • fragment 中使用 ViewPager 時應該使用getChildFragmentManager

錯誤的在 fragment 中使用 activityFragmentManager 會引發內存泄露。 為什么呢?假如您的fragment中有一些依靠 ViewPager 管理的子 fragment,並且所有這些 fragment 都在 activity 中,因為您使用的是activityFragmentManager 。 現在,如果關閉您的父fragment,它將被關閉,但不會被銷毀,因為所有子fragment都處於活動狀態,並且它們仍在內存中,從而導致泄漏。 它不僅會泄漏父fragment,還會泄漏所有子fragment,因為它們都無法從堆內存中清除。

FragmentStateAdapter 和 FragmentPagerAdapter

FragmentPagerAdapter將整個 fragment存儲在內存中,如果ViewPager中使用了大量 fragment,則可能導致內存開銷增加。 FragmentStatePagerAdapter僅存儲片段的savedInstanceState,並在失去焦點時銷毀所有 fragment

讓我們看看常見的兩個問題

1. 刷新ViewPager不生效

ViewPager 中的 fragment 是通過 activityfragmentFragmentManager 管理的,FragmentManager 包含了viewpager的所有fragment的實例

因此,當ViewPager沒有刷新時,它只是FragmentManager仍保留的舊 fragment 實例。 您需要找出為什么FragmentManger持有fragment實例的原因。

2. 在Viewpager中訪問當前fragment

這也是我們遇到的一個非常普遍的問題。 如果遇到這種情況,我們一般在 adapter 內部創建 fragment 的數組列表,或者嘗試使用某些標簽訪問fragment。 不過還有另一種選擇。 FragmentStateAdapterFragmentPagerAdapter都提供方法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添加到容器中。 假設您將FragmentAFragmentB添加到容器中。 容器將具有FragmentAFragmentB,如果容器是FrameLayout,則將fragment一個添加在另一個之上。

replace將簡單地替換容器頂部的一個fragment,因此,如果我創建了 FragmentCreplace 頂部的 FragmentB,則FragmentB將被從容器中刪除(執行onDestroy,除非您調用addToBackStack,僅執行onDestroyView),而FragmentC將位於頂部。

那么如何選擇呢? replace刪除現有fragment並添加一個新fragment。 這意味着當您按下返回按鈕時,將創建被替換的fragment,並調用其onCreateView。 另一方面,add保留現有fragment,並添加一個新fragment,這意味着現有fragment將處於活動狀態,並且它們不會處於 “paused” 狀態。 因此,按下返回按鈕時,現有fragment(添加新fragment之前的fragment)不會調用onCreateView。 就fragment的生命周期事件而言,在replace的情況下將調用onPauseonResumeonCreateView和其他生命周期事件,在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多次添加?

當我們使用BottomBarNavigationNavigationDrawer時,通常會看到諸如fragment 重建或多次添加相同fragment之類的問題。

在這種情況下,您可以使用show / hide 而不是 addreplace

返回棧

如果您想在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 屬性允許您添加fragmentandroid: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)
    }
}

fragmentFragmentManager 管理,因此很自然,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"

對於ViewPager2TabLayout布局應與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時,TabLayoutViewPager聯動需要調用 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,數據庫,網絡,算法等,抽象且難以理解,大學時不學,以后就很難拾起來。

既強調動手,又強調抽象,二者缺一不可。但善於思考的人,往往喜歡謀定而后動;善於行動的人,往往沒功夫回顧思考。

對於要先理解才動手的人,是種折磨。往往做了一兩年,才突然理解某個概念。

對於初學者,難以區分學的知識,還是配置。

雜訊太多,不知道學什么。

總得來說,編程里最簡單的地方往往價值不高,困難的地方這次避開了,下次還是要理解,逃也逃不掉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM