UI组件化方案的思考

UI组件化方案的思考

一、 背景

平时在开发大量业务的时候,是否经常会有这样感受: 这个页面貌似以前写过,这块UI界面和我之前写的太像了吧。你会发现很多业务UI界面都是很类似,而且有些甚至一样,但是这些UI页面逻辑确实很像,可是它们都来自不同的数据源,甚至不同模块业务的数据源。这可咋复用啊,算了还是再写一遍,写着写着你就开始复制代码了,因为这里面UI交互逻辑太像了,几乎一样。作者我就是经常会有这样感觉,于是开始大胆设想: 有没有这样一种,把一个页面按照不同的粗细粒度进行分块,拆解;然后每个块都是一个独立、可测试、可插拔、可复用的组件,不与具体实际业务绑定,只负责UI的渲染, 注入它所需渲染UI模型,向外暴露事件回调。然后下次遇到相同的业务只需要简单安装这个组件到业务页面视图中、注入所需要UI数据模型、处理外部抛出UI事件。这样每次开发需求不再是重新写重复的页面了,只需要想搭积木一样实现UI页面,快速开发,稳定高效

二、市面上类似的方案

实际上,关于UI组件化,向搭积木一样去构建你的UI业务页面;是我早在几年前,刚进公司面试的时候,就提出了。可是令我自己都觉得神奇的是,美团技术团队与我的思想不谋而合,后来两年后美团就开源了 移动端页面模块化开发框架Shield 它的主要思想90%都和我的思想类似。

首先,它也是将当前页面按照不同的粗细粒度进行切分成多个模块,分解后就可以进一步降低页面逻辑的复杂度。随着业务的不断发展,根据特定业务场景产生的定制化需求变得越来越多。单一页面往往需要根据不同业务、不同场景甚至不同用户展示不同的内容。在这样的背景下,我们开始考虑对页面进行切分,把一个页面切分成多个模块,以提高复杂页面的可维护性

Shield是美团点评到店综合团队模块化UI界面解决方案,它不仅仅是一个Native(Android&iOS)的UI开发框架,还是到店综合团队基于自身复杂的业务场景沉淀出来的UI开发最佳实践。它具备高可复用、容易协同开发等特性,还包括后端动态配置、动态模块等一系列解决方案,目前已经在GitHub上开源:https://github.com/Meituan-Dianping/Shield

在Shield框架里,页面是由一个个模块(Agent)组成的。模块是页面中粗粒度的抽象组件,包含部分页面UI展示和与之相关的业务逻辑。这些模块按线性的方式排布在页面中,可以很灵活地调换位置且互不影响。每个模块都有自己独立的生命周期,可以单独通过网络获取数据、渲染视图等等。(但是单独通过网络数据,渲染视图,这一点我和它不一样)

此外,模块化的拆分与MVP等架构方式的拆分并不冲突。开发者完全可以在Shield的某个模块里运用MVP或MVVM的架构方式,来对页面的逻辑进行进一步的拆分以提升代码复用性,使模块逻辑变得更加清晰。

注意: 但是我和它框架思想唯一不一样的地方就是,这个UI组件只负责UI逻辑渲染,不负责网络请求,它有自己的生命周期。因为一旦涉及网络API,它必定就很容易和具体的业务绑定在一起,那么复用性就不会很强,它的职责其实很单一,就只负责UI层渲染,然后向外抛出事件。只负责渲染的话,它有自己的UI层独有的视图模型,那么任何一个数据源,想要用这个组件,只需要写一个数据模型转化器,把源数据转化成视图模型就行了

三、UI组件化思想

1、基本介绍

为了更好地解决多业务复用以及提高分解复杂页面维护性问题,我们推出UI组件化方案。它的思想是:针对一个业务布局页面,可以根据业务不同粗细粒度把页面进行切分,切分成若干个模块,我们把这样一个模块称为Component,每个模块都有自己独立的生命周期, 不负责网络请求,只负责UI渲染逻辑,然后负责把UI层交互事件抛到外部即可。为了更好解耦和复用,每个Component都会有一个所谓的ViewModel与之对应,ViewModel是从UI视图抽象出的只属于Component的视图模型。然后不同业务不同数据想使用这个组件,只需要写一个数据模型转化器Converter,将原数据转化成Component的视图模型即可

2、结构组成

在UI组件化方案问题域中,我们定义了三个概念: Component(组件)、ViewModel(组件视图模型)、Converter(组件视图模型转化器) 。如下图所示,就是多个数据源或业务复用同一Component模型。它还存在一个好处就是只要产品层面定义视图模型足够稳定,不管外部的数据源怎么变化,后端API怎么变化,都不会影响底部Component组件以及组件模型,可以只需要简单修改外部Converter(组件视图模型转化器)即可

3、开发思维的转变(逆向思维)

为什么说这是一个逆向思维呢? 这是因为一般开发UI时候,我们总是写后端API,然后对着设计图直接XML画UI,然后去写我们逻辑。但是在UI组件化开发思路却是逆向的。

首先你需要对照设计图深入理解业务视图交互流程,按照粗细粒度合理地对页面进行切分;然后,针对切分好的模块,进行视图模型建立和抽取,比如页面显示元素,以及页面会涉及到哪些UI交互事件。建立好视图模型ViewModel后,然后就可以去创建Component,然后单独创建这个Component视图,然后根据ViewModel去渲染Component视图,并把Component视图事件回调外部;然后再把这个Component装载到Activity、Fragment或者一个特定视图中,最后外部只需要写一个Converter,通过它把API数据转化成ViewModel, 并设置这个Component事件回调即可.

实际上,这个开发思维不仅仅适用于移动端,个人认为只要涉及到UI视图程序都可以适用,页面独立拆分,实际上现成例子就是Flutter框架,在开发Flutter的时候,都是不断在视图添加若干Widget, 每个Widget都是独立的,而这里就是添加Component,每个Component都是独立的。

4、样例代码

//ComponentTrainingCard
class ComponentTrainingCard(
        context: Context,
        parent: ViewGroup,
        isRemoveAll: Boolean
) : ComponentBase<VModelTrainingCard>(//VModelTrainingCard绑定的视图模型
        context = context,
        parent = parent,
        isRemoveAll = isRemoveAll,
        contentView = parent.inflate(R.layout.biz_daily_paper_component_training_card)//渲染的视图的布局
) {
    override fun onViewCreated() {
        //初始化视图样式
    }
    override fun onViewEventTriggered() {
        //处理视图所有UI事件监听
     mViewRoot.training_card_btn_finished.setOnAntiShakingClickListener {
            if (::mListener.isInitialized) {
                mListener.mFinishBtnClickedAction?.invoke()
            }
        }
    }
    override fun renderWidget(viewModel: VModelTrainingCard) {
        //供外部调用,传入渲染所需的视图模型
    }

    //以下就是抛到外部的事件监听
    private lateinit var mListener: ListenerBuilder

    fun setListener(listenerBuilder: ListenerBuilder.() -> Unit) {
        mListener = ListenerBuilder().also(listenerBuilder)
    }

    inner class ListenerBuilder {
        internal var mFinishBtnClickedAction: (() -> Unit)? = null
        fun onFinishBtnClicked(action: () -> Unit) {
            mFinishBtnClickedAction = action
        }
    }
}

//外部Activity 使用
//创建组件对象
 private val mComponentTrainingCard: ComponentTrainingCard by lazy {
        ComponentHeadline(this, page_layout_content, false)
 }
 //初始化组件
 private fun setupComponents() {
     mComponentTrainingCard.initComponent();
 }
 //设置组件监听
 private fun initEvent() {
      mComponentTrainingCard.setListener {
            onFinishBtnClicked {
                mComponentTrainingPage.nextPage()
            }
      }
 }
 //渲染组件
 private fun renderComponents() {
     mComponentTrainingCard.renderWidget(trainingPage.sentenceCard)
 }

5、优点

针对UI组件化方案优点,个人觉得还是很多的

  • 独立、业务逻辑高内聚低耦合、可复用

  • 独立、测试起来就很简单.

  • 把复杂页面分解成一个个子模块,无疑会降低整个页面复杂度

  • 灵活性高、只需要写一个模型转化器,可配置性高、可插拔使用

  • 维护成本低,不管外部业务数据源,后端API怎么变化,都不会影响到内部Component的逻辑,只需修改外部模型转化器即可

  • 可支持多个不同业务,不同数据源,复用同一个Component

  • 降低代码出错率,复用性强就意味代码量变少,那么出错率自然就低了。

6、缺点

由于需要针对不同数据源都要复用同一个组件,每个数据源都需要写一个数据转化器,此外每个视图组件都会有一个自己View层视图模型。所以单次需求的开发代码量会比直接干那种方式会多点。模板代码会多一点,针对这问题,后面特地做了一个代码脚手架生成模板代码。

四、UI组件化概念图

五、模板代码脚手架

为了解决每写一个组件都需要去写一个ViewModel视图模型以及会去写一个Convert视图模型转化器。特地为这个写一个AndroidStudio代码模板,可以就能通过IDE可视化界面一键创建一个UI组件Component以及相关ViewModel模型和转化器,无需手动创建,就类似创建Activity一样创建自己的Component即可,非常简单方便。

六、总结

到这里,有关UI组件化思考就这些,目前这个方案已经在使用;同时和IOS一起明显整体效率、开发速度以及代码质量都会比好一点。


   转载规则


《UI组件化方案的思考》 mikyou 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Android性能优化之内存管理 Android性能优化之内存管理
Android性能优化之内存管理一、内存分配说到Android的内存分配,就不得不提Java中的内存管理。Java程序在运行时将数据划分为若干不同数据区: 方法区(线程共享)、堆区-heap区(线程共享)、虚拟机栈(线程私有)、本地方法栈(
2019-12-27
下一篇 
MVP通信方案的思考 MVP通信方案的思考
MVP通信方案的思考一、简述MVC、MVP、MVVM区别市面上关于MVC、MVP、MVVM各种实现方案都不一样,下面简要说下MVC、MVP、MVVM之间的区别。 1、MVC模式 就是把软件分为三个部分: Model(数据模型层)、View(
2019-12-23