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="@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的结构:

classDiagram

IAbility <|.. AbsListAbility
AbsListAbility <|-- ListAbility
AbsListAbility <|-- PagerAbility

class IAbility {
+onCreate(LifecycleOwner)
+onResume(LifecycleOwner)
+onPause(LifecycleOwner)
+onDestroy(LifecycleOwner)
}

class AbsListAbility {
#listHost: IList
#viewModel:BaseViewModel

+onCreate(LifecycleOwner)
#providerEmptyView(): IEmptyView
#setupItemBinders(ArrayList)
#setupItemDecoration(ItemDecoration)
}

class ListAbility {
+ viewModel: BaseViewModel
+ listHost: IRecyclerHost

+providerEmptyView(): IEmptyView
+setupItemBinders(ArrayList)
+setupItemDecoration(ItemDecoration)
}

class PagerAbility {
+ viewModel: BaseViewModel
+ listHost: IPagerHost

+providerEmptyView(): IEmptyView
+setupItemBinders(ArrayList)
+setupItemDecoration(ItemDecoration)
}

class IListHost {
+providerAdapter():RecyclerView.Adapter
+autoRefresh():Boolean
+setupEmptyView(EmptyBuilder)
+providerEmptyView(Context):IEmptyView
+decorationBuilder():ItemDecoration.Builder.() -> Unit
+registerItemBinders(ArrayList)
}


class IRecyclerHost {
+recyclerView: RecyclerView
+providerLayoutManager():RecyclerView.LayoutManager
}

class IPagerHost {
+viewPager: ViewPager2
}

IRecyclerHost ..<|IListHost
IPagerHost .. <| IListHost

ListAbility <-- IRecyclerHost
PagerAbility <-- IPagerHost

如图所示,AbsListAbility实现了IAbility,也就拥有了生命周期感知能力。当onCreate()被调用时可在此做些初始工作(比如订阅数据变动、设置装饰Decoration等)。若是RecyclerView和ViewPager2差异部分,则通过AbsListAbility的直接子类ListAbility和PagerAbility设置(例如设置Adapter,LayoutManager等)。

AbsListAbility需要的一些信息通过IListHost接口获取,IListHost接口定义了需要为列表提供的参数或者将在合适的时机通知页面状态变更的方法。

interface IListHost {

    /**
     * 如果需要提供新Adapter, 重写此方法返回需要设置的Adapter
     * 否则不需要重写此方法,默认使用BaseBinderAdapter
     */
    fun providerAdapter(): RecyclerView.Adapter<*>? = null

    /**
     * 是否进入页面立即获取数据
     */
    fun autoRefresh() = true

    /**
     * 配置空页面
     */
    fun setupEmptyView(builder: EmptyBuilder) {
    }

    /**
     * 如果不使用默认的空页面,重写并返回其他空页面
     * 如果不需要空页面返回null;
     */
    fun providerEmptyView(context: Context): IEmptyView? = EmptyView(context)

    /**
     * 配置列表分割线
     */
    fun decorationBuilder(): ItemDecoration.Builder.() -> Unit {
        return { }
    }

    /**
     * 添加列表item样式
     */
    fun registerItemBinders(binders: ArrayList<BaseRecyclerItemBinder<out Any>>) {
    }
}
复制代码

四、结语

以上分析一句话概括就是在Activity中注册ListAbility并提供IListHost,ListAbility在onCreate()方法中做初始操作,所需的参数或者状态变更回调都通过IListHost。看完你可能会说:“就这?”。哈哈,原理确实很简单,分享的更多是一种思路,一种实现方式。不过这确实让我摆脱了BaseListxxx,也解决了我遇到的高耦合难扩展问题,页面的代码编写也是非常的简单,能偷懒就绝不多写一行代码是我的编程宗旨。如果你有好的建议或意见,还请不吝赐教!!!写得不好请多见谅。

至于数据订阅和分页功能等功能可参考Feature,空页面实现可看这《MVVM框架搭建之——简单实现不同状态的空页面》

MVVM框架搭建系列

《MVVM框架搭建之——如何优化多层继承造成的耦合问题(一)》

《MVVM框架搭建之——简单实现不同状态的空页面(三)》

更多待续,欢迎关注。。。

分类:
Android
标签: