MVVM框架搭建之——列表页面的高效实现

一、前言

带有列表的页面是App中最常见的页面之一,这些页面有着高度相似的地方。比如一般都会有刷新功能、需要分页加载、需要处理数据异常情况、没内容时要展示空页面等等。开发者一般都会将功能类似,重复使用的功能封装起来,很多人的做法是创建BaseListActivity/Fragment。这本没啥问题,但考虑一种情况,当项目中有几十个列表页面,突然有一天需要对BaseList功能进行更改,他的子类也必须要变动,那么对于更改这几十个子页面来说无疑是令人崩溃的,这就是继承带来的副作用——高耦合。优化方案可参考这篇文章,这里我们探讨如何不使用继承简单高效封装列表功能。

二、页面实现

假设需要实现这样一个页面,我们先实现在讲细节吧。

list.png

1. 开撸之前先对页面构成简要分析:

    1. 顶部是由ViewPager2实现的自动轮播Banner。
    1. 可左右切换页面的ViewPager2。
    1. 带有刷新和分页功能的标准列表子页面。

2.上代码(这里只展示列表UI相关的完整代码,数据获取部分可看demo)

    1. xml编写
// Banner
<androidx.viewpager2.widget.ViewPager2
   android:id="@+id/viewPager"
   android:layout_width="match_parent"
   android:layout_height="200dp"
   // 自动滚动
   app:auto_scroll="@{true}"
   // 添加item样式
   app:itemBinderName='@{"com.rongc.wan.ui.WanBannerBinder"}'
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent"
   // 自动轮播
   app:loop="@{true}"
   // 绑定数据
   app:items="@{viewModel.banners.data}"
   // 滚动间隔
   app:scroll_interval="@{5000}" />
  
<com.google.android.material.tabs.TabLayout
   android:id="@+id/tabStrip"
   style="@style/MyTablayoutstyle"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   app:tabMode="scrollable"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toBottomOf="@id/viewPager" />
   
<androidx.viewpager2.widget.ViewPager2
   android:id="@+id/pagerList"
   android:layout_width="match_parent"
   android:layout_height="0dp"
   app:items="@{viewModel.tabs}"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintTop_toBottomOf="@id/tabStrip" />
复制代码
    1. 页面编写(顶部的banner在xml中就已经完成了,这里仅需关注可切换ViewPager2的实现)
class WanHomeFragment : BaseFragment<FragmentWanHomeBinding, WanHomeViewModel>(), IPagerHost {
    
    /**
     * 返回需要PagerAbility的ViewPager
     */
    override val viewPager: ViewPager2 get() = mBinding.pagerList

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // 注册PagerAbility
        registerAbility(PagerAbility(viewModel, this))

        // 分类数据请求结果订阅,ignoreLoading()为忽略加载中状态只关心结果
        // whenSuccess,只在请求成功后接收通知
        viewModel.result.whenSuccess(lifecycleOwner) {
            // 配置TabLayout
            it.data?.forEach { project ->
                val tab = mBinding.tabStrip.newTab()
                tab.text = project.name
                mBinding.tabStrip.addTab(tab)
            }

            TabLayoutMediator(
                mBinding.tabStrip, mBinding.pagerList, true, true
            ) { tab, position ->
                tab.text = it.data?.getOrNull(position)?.name ?: ""
            }.attach()
        }
    }

    /**
     * 若ViewPager2内容非View(是Fragment)时重载此方法返回BaseFragmentPagerAdapter
     * 否则不需要提供Adapter
     */
    override fun providerAdapter(): RecyclerView.Adapter<*> {
        return object : BaseFragmentPagerAdapter<String>(this) {
            override fun createItemFragment(item: String, position: Int): IPagerItem<String> {
                // 根据position返回子页面
                return ProjectListFragment().apply {
                    arguments = bundleOf("cid" to item)
                }
            }
        }
    }

    /**
     * BaseFragmentPagerAdapter照样支持空页面
     */
    override fun setupEmptyView(builder: EmptyBuilder) {
        builder.whenDataIsEmpty {
            tip = "no data found"
        }
    }
}
复制代码
  • 3.子列表页面Fragment
class ProjectListFragment : BaseFragment<BaseRecyclerWithRefreshBinding, ProjectListViewModel>(),
    IPagerItem<String>, IRecyclerHost {

    override val recyclerView: RecyclerView get() = mBinding.recyclerView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.cid.value = arguments?.getString("cid")
        // 注册ListAbility实现列表相关功能
        registerAbility(ListAbility(viewModel, this))
    }

    override fun registerItemBinders(binders: ArrayList<BaseRecyclerItemBinder<out Any>>) {
        // 添加列表Item样式Binder
        // 有几种ItemType就添加几个对应的ItemBinder
        binders.add(ProjectItemBinder())
    }
}
复制代码
  • 4.列表Item xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <!-- 如定义了名称为‘bean’的属性,Binder会自动为它赋值 -->
        <variable
            name="bean"
            type="com.rongc.wan.ProjectList" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/iv_project"
            android:layout_width="120dp"
            android:layout_height="250dp"
            android:layout_marginStart="15dp"
            android:layout_marginTop="@dimen/dp_10"
            android:scaleType="centerCrop"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:url="@{bean.envelopePic}"
            tools:src="https://juejin.cn/post/@color/black_30" />

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="15dp"
            android:layout_marginTop="@dimen/dp_10"
            android:layout_marginEnd="15dp"
            android:ellipsize="end"
            android:maxLines="2"
            android:text="@{bean.title}"
            android:textColor="#353535"
            android:textSize="16sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/iv_project"
            app:layout_constraintTop_toTopOf="@id/iv_project"
            tools:text="project name" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="20dp"
            android:ellipsize="end"
            android:text="@{bean.desc}"
            android:textColor="@color/black_40"
            app:layout_constraintBottom_toTopOf="@id/tv_author"
            app:layout_constraintEnd_toEndOf="@id/tv_name"
            app:layout_constraintStart_toStartOf="@id/tv_name"
            app:layout_constraintTop_toBottomOf="@id/tv_name"
            app:layout_constraintVertical_bias="0"
            tools:text="describe" />

        <TextView
            android:id="@+id/tv_author"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:text="@{bean.author}"
            app:layout_constraintBottom_toBottomOf="@id/iv_project"
            app:layout_constraintStart_toStartOf="@id/tv_name"
            tools:text="author" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{bean.publishTimeStr}"
            app:layout_constraintBaseline_toBaselineOf="@id/tv_author"
            app:layout_constraintEnd_toEndOf="@id/tv_name"
            tools:text="time" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码
  • 5.列表ItemBinder
class ProjectItemBinder : BaseItemBindingBinder<ItemProjectListBinding, ProjectList>() {
    override fun convert(
        binding: ItemProjectListBinding, holder: BaseViewHolder, data: ProjectList
    ) {
        // ui绑定都在xml中做好,这里不需要其他实现
    }
}
复制代码

以上就是UI相关的全部代码,这是运行效果
viewpager.gif

如果仅需要实现列表页面则只有3、4、5三步。

三、详解

《如何优化多层继承造成的耦合问题》中提到利用Lifecycle的生命周期感知能力优化通过聚合方式扩展功能的使用体验——IAbility。这里的列表功能也是Ability的其中一种(ListAbility/PagerAbility)。ListAbility的主体是RecyclerView,PagerAbility的主体时ViewPager2,其他几乎一致,这里以ListAbility作分析。

先看下ListAbility的结构:

IAbility+onCreate(LifecycleOwner)+onResume(LifecycleOwner)+onPause(LifecycleOwner)+onDestroy(LifecycleOwner)AbsListAbility#listHost: IList#viewModel:BaseViewModel+onCreate(LifecycleOwner)#providerEmptyView()#setupItemBinders(ArrayList)#setupItemDecoration(ItemDecoration)ListAbility+ viewModel: BaseViewModel+ listHost: IRecyclerHost+providerEmptyView()+setupItemBinders(ArrayList)+setupItemDecoration(ItemDecoration)PagerAbility+ viewModel: BaseViewModel+ listHost: IPagerHost+providerEmptyView()+setupItemBinders(ArrayList)+setupItemDecoration(ItemDecoration)IListHost+providerAdapter()+autoRefresh()+setupEmptyView(EmptyBuilder)+providerEmptyView(Context)+decorationBuilder()+registerItemBinders(ArrayList)IRecyclerHost+recyclerView: RecyclerView+providerLayoutManager()IPagerHost+viewPager: ViewPager2

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享