TabLayoutMediator
和FragmentStateAdapter
,只需常常运用 TabLayout 和 ViewPager2 ,这两个东西你必定不会生疏。前者用来使 TabLayout 和 ViewPager2 进行结合,后者用来设置 ViewPager2 的适配器。
前者咱们应该用的都没什么问题,只需不要忘记在最终调用TabLayoutMediator#attach()
办法。
后者问题就比较多了,乃至在 StackOverflow 上也有不少像这样用错的。
过错用法
举一个十分简略的比如,你需求给 ViewPager2 绑定几个以下需求的 Fragment :
- FirstFragment,需求给它传一个名为
name
的参数,值为Ace Taffy
。 - SecondFragment,需求给它传一个名为
position
的参数,值为1
。 - 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 组成,可是需求分别传downloaded
、downloading
、failed
三个参数才干触发到相应的请求。
根据上面的过错,咱们能够写出这样的代码
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 的了,但你有没有觉得这样做很搞笑呢?