【背上Jetpack之Fragment】你真的会用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()

demo地址


关于我


我是 Fly_with24

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容