viewpager2中viewModelScope 取消的问题
场景
有这么一个场景,一个菜谱订制的app里,用户是根据每周作为一个周期制定自己的菜谱计划,每天从已知菜谱库存中选一两道菜,规划自己下周做什么吃,下下周做什么吃。
viewpager(或viewpager2)中加载若干个fragment,fragment里被传入一个时间戳的参数,用这个时间戳当做初始时间,计算出本周的起止时间(周日~周六),然后获取这一周的菜谱计划。类似于这样(demo式UI)
上图中标注a点击后切到上一周(viewpager2中前一个fragment),b点击切到下一周,下方列表为viewpager2(若干fragments)当然列表显示什么不重要
viewpagerAdapter 类似这样,用list维护一个Fragment
class MealPagerAdapter(fm: FragmentManager?, life : Lifecycle) : FragmentStateAdapter(fm!! , life) {
val fragmentList = mutableListOf<MealFragment>()
val startDateTimeList = mutableListOf<Long>()
fun addItem(startDate : Long) {
startDateTimeList.add(startDate)
fragmentList.add(MealFragment.newInstance(startDate))
notifyItemInserted(fragmentList.size - 1)
}
override fun getItemCount(): Int = fragmentList.size
override fun createFragment(position: Int): MealFragment = fragmentList[position]
}
默认初始调用一次addItem方法,参数传入当前周(周日开始)的周日0点的时间戳,就可以获得这一周的起止日期了,这是一个可以无限添加fragment的viewpager2, 所以
offscreenPageLimit = 5
设置多少不那么重要了,暂定5吧
MealFragment里就是 viewmodel中定义一个initData()方法,伪代码示意:
class MealViewModel : BaseViewModel() {
fun init() {
viewModelScope.launch {
// 请求数据
...
}
}
}
class MealFragment : Fragment() {
val viewModel : MealViewModel by viewModels()
onverried fun onViewCreated() {
viewmodel.init()
}
}
现在是不是已经看出问题来了。
现象是多次点击【下一周】按钮 添加几个fragment,只要超出了设置的离屏缓存数量,往回滑,之前显示过的fragment会重新加载,因为viewpager移除了fragment,不过重新经过onViewcreated 周期的时候,viewmodel里的 viewmodelScope 不再执行,导致页面空白
viewModelScope.launch {
// 不再执行了
...
}
显然,这个协程scope被cancel(close)了,不过没有被从ViewModel的map中移除,
所以也就谈不上重建。
扫一眼ViewModel 源码,内部有个
Map<String, Object> mBagOfTags = new HashMap<>();
用来存储scope
出现这种问题大概权当使用不当吧,虽然我这么用viewpager已经很久了
一些方案
方案1
viewmodel里的viewModelScope 被取消但不被移除,那就暂且不用这个了,替换为GlobalScope 总能用吧
stackoverflow上有一个同样和我一知半解的老外提了同样的问题
未测试
不过这种方案应该没人会采用,globalScope 估计只用于demo测试中
方案2
不自己缓存fragmentlist了,只缓存数据,每次去从viewpager缓存中获取
class MealViewPagerAdapter(fm: FragmentManager?, life : Lifecycle) : FragmentStateAdapter(fm!! , life) {
val startDateTimeList = mutableListOf<Long>()
fun addItem(startDate : Long) {
startDateTimeList.add(startDate)
notifyItemInserted(itemCount - 1)
}
override fun getItemCount(): Int = startDateTimeList.size
override fun createFragment(position: Int): MealItemFragment = MealItemFragment.newInstance(startDateTimeList[position])
}
测试ok
方案3
不使用viewmodelScope, 用fragment的Scope代替
fun launchLifecycle(lifecycleOwner : LifecycleOwner , block: suspend () -> Unit) {
lifecycleOwner.lifecycleScope.launch {
block()
}
}
测试ok
方案4
谷歌官方的示例项目iosched中是这样写的
viewpager2中同样使用list 缓存fragment,但不是缓存实例,只缓存生成方法,createFragment(position: Int) 方法调用的时候调用闭包来获取fragment(invoke)。
/**
* Adapter that builds a page for each info screen.
*/
inner class InfoAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun createFragment(position: Int) = INFO_PAGES[position].invoke()
override fun getItemCount() = INFO_PAGES.size
}
companion object {
private val INFO_TITLES = arrayOf(
R.string.event_title,
R.string.travel_title,
R.string.faq_title
)
private val INFO_PAGES = arrayOf(
{ EventFragment() },
{ TravelFragment() },
{ FaqFragment() }
// TODO: Track the InfoPage performance b/130335745
)
}
尽管他只有三个fragment,不过测了下自己的场景,同样能解决问题
测试ok
代码位于项目中的InfoFragment类里
方案5
上边的stackOverflow方法中还提到用共享viewmodel的方式,大致应该是这样
private val viewModel: SearchViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
让viewmodel去伴随父fragment的周期,不过感觉设计上不太合适,没有去测
链接:https://juejin.cn/post/7064185160299708446
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。