注册

viewpager2中viewModelScope 取消的问题

场景


有这么一个场景,一个菜谱订制的app里,用户是根据每周作为一个周期制定自己的菜谱计划,每天从已知菜谱库存中选一两道菜,规划自己下周做什么吃,下下周做什么吃。


viewpager(或viewpager2)中加载若干个fragment,fragment里被传入一个时间戳的参数,用这个时间戳当做初始时间,计算出本周的起止时间(周日~周六),然后获取这一周的菜谱计划。类似于这样(demo式UI)


image.png


image.png


上图中标注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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册