TabLayoutMediatorFragmentStateAdapter,只需常常运用 TabLayout 和 ViewPager2 ,这两个东西你必定不会生疏。前者用来使 TabLayout 和 ViewPager2 进行结合,后者用来设置 ViewPager2 的适配器。

前者咱们应该用的都没什么问题,只需不要忘记在最终调用TabLayoutMediator#attach()办法。

后者问题就比较多了,乃至在 StackOverflow 上也有不少像这样用错的。

过错用法

举一个十分简略的比如,你需求给 ViewPager2 绑定几个以下需求的 Fragment :

  1. FirstFragment,需求给它传一个名为name的参数,值为Ace Taffy
  2. SecondFragment,需求给它传一个名为position的参数,值为1
  3. ThirdFragment,不需求参数。

传值我为了便利,运用一个拓展函数

Fragment.makeBundle(vararg Pair<String, Any>): Fragment

相似于自己在 Fragment 内部写的 static 函数 XXFragment.newInstance(vararg Any)

灵机一动,这不简略吗?先把它们放在 List 里

val fragmentList = listOf(
  FirstFragment().makeBundle("name" to "Ace Taffy"),
  SecondFragment().makeBundle("position" to 1),
  ThirdFragment()
   ...
)

然后构建一个 FragmentStateAdapter,设置 ViewPager2 的 adapter。这儿就不单独给它弄一个 class 了

viewPager2.adapter = object : FragmentStateAdapter(this) {
  override fun getItemCount() = fragmentList.size
  override fun createFragment(position: Int) = fragmentList[position]
}

这么寥寥几行,就完成了构建,看起来十分的简略,要是再想加几个 Fragment 也会很便利。但这样真的对吗?

过错剖析

咱们先来剖析一下 FragmentStateAdapter#createFragment(int)这个函数,看一看官方是怎样界说的

/**
 * Provide a new Fragment associated with the specified position.
 * <p>
 * The adapter will be responsible for the Fragment lifecycle:
 * <ul>
 *   <li>The Fragment will be used to display an item.</li>
 *   <li>The Fragment will be destroyed when it gets too far from the viewport, and its state
 *   will be saved. When the item is close to the viewport again, a new Fragment will be
 *   requested, and a previously saved state will be used to initialize it.
 * </ul>
 * @see ViewPager2#setOffscreenPageLimit
 */
public abstract @NonNull Fragment createFragment(int position);

翻译一下,便是在与其相关的方位上供给一个新的 Fragment。这儿这个 new 十分的显眼,它需求一个 Fragment,并且得是全新的。

再看一下这个抽象函数被哪里引证,能够发现大局只用在ensureFragment(int)这儿

private void ensureFragment(int position) {
  long itemId = getItemId(position);
  if (!mFragments.containsKey(itemId)) {
    // TODO(133419201): check if a Fragment provided here is a new Fragment
    Fragment newFragment = createFragment(position);
    newFragment.setInitialSavedState(mSavedStates.get(itemId));
    mFragments.put(itemId, newFragment);
   }
}

先不必看其他,这个 TODO 就很显眼,乃至官方都还没完美处理如何检测供给的 Fragment 是否为新的 Fragment 这个问题。

这个函数也很简略,假如mFragment(itemId 与 Fragment 相关起来的 LongSparseArray)中没有该页应该有的 Fragment,则经过之前说的 createFragment(int)回调获取 Fragment,然后经过 Fragment#setInitialSavedState(Fragment.SavedState)办法,从 mSavedStates (itemId 与 SavedState 相关起来的 LongSparseArray)中获取该页储存的 SavedState(没有就回来 null),最终把这个加工过的 Fragment 与 itemId 配对加入到mFragment里。

再去翻翻代码,还能够看到gcFragments()的函数,函数如其名,便是收回不必要的 Fragment,所以说 Fragment 是一个收回重建的过程,但你不必定感受的到,由于把 Fragment 的 SavedState 保存了,重建的时候康复一下就能够了。

能够看到,它需求一个新的 Fragment,然后加工成一个咱们真正所需的 Fragment。而在咱们之前的过错用法中(如下),

val fragmentList = listOf(
  FirstFragment().makeBundle("name" to "Ace Taffy"),
  SecondFragment().makeBundle("position" to 1),
  ThirdFragment()
   ...
)

经过编写 list,现已提早给他实例化了,你只需获取到 list 中的元素,那便是引证,而不是深复制(并且Fragment 没有完成Cloneable接口,不支持深复制)。所以在这儿(如下),

override fun createFragment(position: Int) = fragmentList[position]

它需求一个 new Fragment,而咱们一向给它一个现已实例化的 Fragment 的引证,这便是过错所在。

举一个比如,假定你考一次试,这是你第一次见到这张试卷。可是过了一段时间,你的老师为了温习让你再一次做这张试卷。你要是把答案记住直接往上书写,那会有很大的风险,只要抛弃之前的记忆,从头写一遍这张试卷,那才干防止各种不必要的风险。

一向给它一个现已实例化的 Fragment 还有什么问题?那便是资源糟蹋。假定你需求展现 100 个 Fragment,我放进 list,它一次性就给我实例化完了,我可能进软件都划不到 10 个,那剩下 90 个实例化还有什么含义呢?

正确运用

多 Fragment

这儿暂时运用了魔法数字,假如你看着不爽也能够界说几个常量代表方位。


override fun createFragment(position: Int): Fragment {
  return when (position) {
    0 -> FirstFragment().makeBundle("name" to "Ace Taffy")
    1 -> SecondFragment().makeBundle("position" to 1)
    2 -> ThirdFragment()
    3 -> ...
    else -> Fragment()
   }
}
​
override fun getItemCount() = 4

这样就防止了资源糟蹋的问题,划到哪儿就实例化哪儿,并且每次都是全新的 Fragment。

单 Fragment

假如不带参数,那就太简略了,直接return new XXFragment()就完事了。

假定你有一个下载界面,需求展现已下载正在下载失败下载三个界面,由于它们三个的全体结构相似,所以由一个 Fragment 组成,可是需求分别传downloadeddownloadingfailed三个参数才干触发到相应的请求。

根据上面的过错,咱们能够写出这样的代码

DownloadFragment#newInstance(String): DownloadFragment是在 DownloadFragment 中界说好的静态函数,便利传参实例化。与上文的Fragment.makeBundle(vararg Pair<String, Any>): Fragment效果一致。

override fun createFragment(position: Int): Fragment {
  return when (position) {
    0 -> DownloadFragment.newInstance("downloaded")
    1 -> DownloadFragment.newInstance("downloading")
    2 -> DownloadFragment.newInstance("failed")
    else -> Fragment()
   }
}
​
override fun getItemCount() = 3

这样写确实没问题,但也能够这样,从 DownloadFragment 中添加静态 List

companion object {
  val typeList = listOf("downloaded", "downloading", "failed")
}

然后直接

override fun createFragment(position: Int) =
    DownloadFragment.newInstance(DownloadFragment.typeList[position])
​
override fun getItemCount() = DownloadFragment.typeList.size

这样想添加新的界面也很简略,往typeList里加新的 type 就完事了,不必再改这改那了。

另辟蹊径

运用 Kotlin 高阶函数特性(Java 的单办法接口也能够),回来的都是 new Fragment,能够想到这种办法来构建

typealias HandleFragment = () -> Fragment
​
open class SimpleViewPagerAdapter(
  fragmentManager: FragmentManager,
  lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {
  
  private val mFragmentList = mutableListOf<HandleFragment>()
​
  override fun getItemCount(): Int {
    return mFragmentList.size
   }
​
  override fun createFragment(position: Int): Fragment {
    return mFragmentList[position].invoke()
   }
​
  fun add(fragment: HandleFragment): SimpleViewPagerAdapter {
    mFragmentList.add(fragment)
    return this
   }
​
  fun add(fragmentList: List<HandleFragment>): SimpleViewPagerAdapter {
    mFragmentList.addAll(fragmentList)
    return this
   }
}

运用起来也很便利

vp.adapter = SimpleViewPagerAdapter(childFragmentManager, lifecycle).apply {
  add { FirstFragment().makeBundle("name" to "Ace Taffy") }
  add { SecondFragment().makeBundle("position" to 1) }
  add { ThirdFragment() }
}

这样更加简化,修正方位不必修正数字了,改动一下 add 的相对方位就能够了。

总结

要是想把 position 那个参数和 list 结合起来,最多把传参组成个 list,然后 new Fragment 的时候把传参放进去。切忌 list 里放实例化的 Fragment 传给 FragmentStateAdapter! 你既不能深复制 Fragment,又会造成资源糟蹋。

聪明的你非要把 Fragment 存个 list,然后说这样不就确保每次都为 new 了吗?(如下)

override fun createFragment(position: Int) = fragmentList[position].javaClass.newInstance()

呃呃,实例化完了还要反射实例化,这多重功能开支我就不说了。并且运用反射的newInstance()根本不好进行参数传递。

聪明的你又想到,我虽然不能深复制 Fragment,但我能够在createFragment(int)里深复制List<Fragment>啊!那你更逆天了,这样的话每调用一次createFragment(int)就会把 list 里的所有元素深复制一遍,这样确实能确保它时间是 new 的了,但你有没有觉得这样做很搞笑呢?