Android开发中ViewPager+Fragment的懒加载
TabLayout+ViewPager+Fragment是我们开发常用的组合。ViewPager的默认机制就是把全部的Fragment都加载出来,而为了保障一些用户体验,我们使用懒加载的Fragment,就是让我们再用户可见这个Fragment之后才处理业务逻辑。
而我们在一些设备或版本中可能就出现懒加载失效的问题。其实谷歌早就把一些懒加载的方案都标记弃用了,我们一直都用的老的随时会失效的Api。万一哪天彻底失效了就会导致线上事故。
接下来我们就看看Fragment的懒加载是如何演变的。谷歌又是推荐我们如何使用的。
1. Support时代的懒加载
在AndroidX还没出来的时候,大家的懒加载应该都是这样。判断setUserVisibleHint的方法,当用户可见的时候才回调方法去加载逻辑。
例如的我封装:
abstract class BaseVDBLazyLoadingFragment : AbsFragment() {
protected lateinit var mViewModel: VM
protected lateinit var mBinding: VDB
private var isViewCreated = false//布局是否被创建
private var isLoadData = false//数据是否加载
private var isFirstVisible = true//是否第一次可见
protected lateinit var mGLoadingHolder: Gloading.Holder
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isViewCreated = true
init()
startObserve()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (isFragmentVisible(this) && this.isAdded) {
if (parentFragment == null || isFragmentVisible(parentFragment)) {
onLazyInitData()
isLoadData = true
if (isFirstVisible) isFirstVisible = false
}
}
}
//使用这个方法简化ViewModewl的Hilt依赖注入获取
protected inline fun getViewModel(): VM {
val viewModel: VM by viewModels()
return viewModel
}
//反射获取ViewModel实例
private fun createViewModel(): VM {
return ViewModelProvider(this).get(getVMCls(this))
}
override fun setContentView(container: ViewGroup?): View {
mViewModel = createViewModel()
//观察网络数据状态
mViewModel.getActionLiveData().observe(viewLifecycleOwner, stateObserver)
val config = getDataBindingConfig()
mBinding = DataBindingUtil.inflate(layoutInflater, config.getLayout(), container, false)
mBinding.lifecycleOwner = viewLifecycleOwner
if (config.getVmVariableId() != 0) {
mBinding.setVariable(
config.getVmVariableId(),
config.getViewModel()
)
}
val bindingParams = config.getBindingParams()
bindingParams.forEach { key, value ->
mBinding.setVariable(key, value)
}
return mBinding.root
}
abstract fun getDataBindingConfig(): DataBindingConfig
abstract fun startObserve()
abstract fun init()
abstract fun onLazyInitData()
//Loading Create Root View
override fun transformRootView(view: View): View {
mGLoadingHolder = generateGLoading(view)
return mGLoadingHolder.wrapper
}
//如果要替换GLoading,重写次方法
open protected fun generateGLoading(view: View): Gloading.Holder {
return Gloading.getDefault().wrap(view).withRetry {
onGoadingRetry()
}
}
protected open fun onGoadingRetry() {
}
override fun onNetworkConnectionChanged(isConnected: Boolean, networkType: NetWorkUtil.NetworkType?) {
}
// ============================ Lazy Load begin ↓ =============================
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isFragmentVisible(this) && !isLoadData && isViewCreated && this.isAdded) {
onLazyInitData()
isLoadData = true
}
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
//onHiddenChanged调用在Resumed之前,所以此时可能fragment被add, 但还没resumed
if (!hidden && !this.isResumed)
return
//使用hide和show时,fragment的所有生命周期方法都不会调用,除了onHiddenChanged()
if (!hidden && isFirstVisible && this.isAdded) {
onLazyInitData()
isFirstVisible = false
}
}
override fun onDestroy() {
super.onDestroy()
isViewCreated = false
isLoadData = false
isFirstVisible = true
}
/**
* 当前Fragment是否对用户是否可见
* @param fragment 要判断的fragment
* @return true表示对用户可见
*/
private fun isFragmentVisible(fragment: Fragment?): Boolean {
return !fragment?.isHidden!! && fragment.userVisibleHint
}
}
使用的示例:
mBinding.viewPager.bindFragment(
supportFragmentManager,
listOf(LazyLoad1Fragment.obtainFragment(), LazyLoad2Fragment.obtainFragment(), LazyLoad3Fragment.obtainFragment()),
listOf("Demo1", "Demo2", "Demo3")
)
mBinding.tabLayout.setupWithViewPager(mBinding.viewPager)
扩展方法:
fun ViewPager.bindFragment(
fm: FragmentManager,
fragments: List,
pageTitles: List? = null,
behavior: Int = 0
): ViewPager {
offscreenPageLimit = fragments.size - 1
adapter = object : FragmentStatePagerAdapter(fm, behavior) {
override fun getItem(p: Int) = fragments[p]
override fun getCount() = fragments.size
override fun getPageTitle(p: Int) = if (pageTitles == null) null else pageTitles[p]
}
return this
}
Fragment:
class LazyLoad1Fragment : BaseVDBLazyLoadingFragment() {
companion object {
fun obtainFragment(): LazyLoad1Fragment {
return LazyLoad1Fragment()
}
}
override fun getDataBindingConfig(): DataBindingConfig {
return DataBindingConfig(R.layout.fragment_demo2)
}
override fun startObserve() {
}
override fun init() {
YYLogUtils.w("LazyLoad1Fragment - init")
mBinding.tvPage2.click {
Demo2Pager2Activity.startInstance()
}
}
//重新生成GLoading对象
override fun generateGLoading(view: View): Gloading.Holder {
return Gloading.from(GloadingRoatingAdapter()).wrap(view).withRetry {
onGoadingRetry()
}
}
override fun onResume() {
super.onResume()
YYLogUtils.w("LazyLoad1Fragment - onResume")
}
override fun onGoadingRetry() {
toast("重试一个请求")
onLazyInitData()
}
override fun onLazyInitData() {
YYLogUtils.w("LazyLoad1Fragment - initData")
//模拟的Loading的情况
showStateLoading()
CommUtils.getHandler().postDelayed({
showStateSuccess()
}, 2500)
}
}
到此就实现了onLazyInitData的回调,只有出现Fragment显示在前台的时候才会调用方法,执行逻辑。
2. AndrodX时代的懒加载
每次判断 setUserVisibleHint 和 onHiddenChanged 也麻烦,并且他们并不稳定,我也遇到过不回调的时候。
Android出来之后,给 FragmentStatePagerAdapter
添加了一个 @Behavior int behavior
的参数。
其本质就是内部帮你处理和切换MaxLifecycle:
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
如何使用呢:
mBinding.viewPager.bindFragment(
supportFragmentManager,
listOf(LazyLoad1Fragment.obtainFragment(), LazyLoad2Fragment.obtainFragment(), LazyLoad3Fragment.obtainFragment()),
listOf("Demo1", "Demo2", "Demo3"),
behavior = 1
)
之前的扩展方法以及预留了 behavior
参数,当为1的时候就不会回调 setUserVisibleHint 方法了,我们直接监听 OnResume 即可。
class LazyLoad3Fragment : BaseVDBLoadingFragment() {
var isLoaded = false
companion object {
fun obtainFragment(): LazyLoad3Fragment {
return LazyLoad3Fragment()
}
}
override fun getDataBindingConfig(): DataBindingConfig {
return DataBindingConfig(R.layout.fragment_demo2)
}
//重新生成GLoading对象
override fun generateGLoading(view: View): Gloading.Holder {
return Gloading.from(GloadingLoadingAdapter()).wrap(view).withRetry {
onGoadingRetry()
}
}
override fun startObserve() {
}
override fun init() {
YYLogUtils.w("LazyLoad3Fragment - init")
}
private fun initData() {
YYLogUtils.w("LazyLoad3Fragment - initData")
//模拟的Loading的情况
showStateLoading()
CommUtils.getHandler().postDelayed({
showStateSuccess()
}, 2500)
isLoaded = true
}
override fun onResume() {
super.onResume()
YYLogUtils.w("LazyLoad3Fragment - onResume")
if (!isLoaded) initData()
}
override fun onGoadingRetry() {
toast("重试一个请求")
initData()
}
}
注意这个页面继承的就不是我们自定义的懒加载Fragment了。普通的Fragment 回调 onResume 即可。
3. ViewPager2时代的懒加载
ViewPager2出来之后。我们的 FragmentStatePagerAdapter
退出历史舞台。
即便能用,即便效果还是和ViewPage2的效果一样,但是还是标记废弃了。具体原因我也不知道,据说是因为老版本会出现问题导致数据丢失,页面空白。
ViewPage2我们都知道内部是通过RV实现的。但是对于Fragment的处理有单独的Adapter实现。
扩展方法:
/**
* 给ViewPager2绑定Fragment
*/
fun ViewPager2.bindFragment(
fm: FragmentManager,
lifecycle: Lifecycle,
fragments: List
): ViewPager2 {
offscreenPageLimit = fragments.size - 1
adapter = object : FragmentStateAdapter(fm, lifecycle) {
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
}
return this
}
使用:
mBinding.viewPager2.bindFragment(
supportFragmentManager,
this.lifecycle,
listOf(LazyLoad1Fragment.obtainFragment(), LazyLoad2Fragment.obtainFragment(), LazyLoad3Fragment.obtainFragment())
)
val title = listOf("Demo1", "Demo2", "Demo3")
TabLayoutMediator(mBinding.tabLayout, mBinding.viewPager2) { tab, position ->
//回调
tab.text = title[position]
}.attach()
使用的方式和ViewPager差不多,这里的Fragment也是使用普通的Fragment即可。
4. ViewPage和ViewPager2的性能对比
内存占用分别取三组数据
ViewPager数据
一。111 二。117.6 三。115.1
ViewPager2数据
一。110 二。107.4 三。107.6
结论 ViewPager2基于RV实现的效果还是比老版ViewPager要骚好一点。
并且老版本标记废弃,大家如果是用ViewPager2的话,还是推荐使用ViewPager2实现。如果大家还是用的老版本的ViewPager也推荐使用behavor参数。使用 onResume
实现懒加载的实现。以后再换到ViewPager2的话,可以无缝切换过来。
说明一下,测试数据仅供参考,毕竟我也不是专业测试,测试数据源也不不多。如有不对的地方,也望大家指正。
好了,关于懒加载就到这里了。源码在此
到此完结。
最后,给大家分享一些大佬整理的学习资料,里面包括Java基础、framework解析、架构设计、高级UI开源框架、NDK、音视频开发、kotlin、源码解析、性能优化等资料,还有2022年一线大厂最新面试题集锦,都分享给大家,助大家学习路上披荆斩棘~ 能力得到提升,思维得到开阔~ 有需要的可以在我的【公众号】免费获取!!!