本文总结于公司新版钱包项目中的设计与实现。
一、需求
首页有多个模块视图,可根据配置动态隐藏或展示。内容超出一屏的时候可滚动展示。
具体包括以下区域:
(1)顶部banner
(2)宫格
(3)中部swiper
(4)通知
(5)活动对话框
(6)不同布局形态的其他动态业务数据
(7)每个模块之间形态各异的分割线
(8)有的模块还需要提供背景阴影
二、常规实现方案
针对以上需求,这里采取先整体后局部的设计思维。首先该页面需要多个不同的模块组成,而且每个模块有自己的不同形态。一般常规实现方案如下:
1、 新建承载页面的activity。
2、 定义布局layout。这个layout包含了以上各个区域,各个区域的View初始态都为gone。
3、 加载layout 并 注册监听事件。在activity中加载layout,并处理点击响应事件。
4、 请求网络数据。根据网络数据来更新layout视图,具体形态可以罗列为:
(1) updateTopBanner 更新顶部banner
(2)updateGride 更新网格数据
(3)updateNotice 更新通知
(5)updateDialog 更新对话框
等等。。。。
这个基本上就是一般的实现方案,而且可以完全实现上面的需求,但是思考一下这个方案有哪些弊端?
三、常规方案弊端
1、activity类会爆掉
所有的数据初始化、更新操作、事件响应操作都放在了这个activity中,这个类会无限膨胀,进而难以维护。比如banner模块有变化的时候,我就要进到这个activity中修改其实现,这违背了软件设计的开闭原则等等。
2、毫无面向对象可言
activity中拿到数据就执行updateXXX方法,典型的面向过程思维。
3、layout会爆掉
将模块的视图布局都放到一个layout中,会让这个layout难以维护。有人可能会将layout进行拆分,然后include到一个layout中,这种方案确实能简化layout布局,但是还不够,比如findViewById的次数没有减少,而且增加了多个layout。
4、可扩展性和可维护性很差
比如我此时要增加一个新的模块,那么就需要在layout添加新模块布局,然后再activity中增加模块相关更新操作,如果有100个怎么办?可扩展性很差,进而可维护性也变得难以下手。
四、优化方案
该方案是针对上述弊端提出的。
1、解决activity类爆掉的问题
利用面向对象的多态合理设计,剥离视图模块(本方案实现可参考模板模式和策略设计模式)
首先由于页面是有多个模块组成,那么就抽象一个公共的模块视图提供接口ViewProvider,接口包含一个方法比如createView(T data),该方法入参是视图更新所需的数据,并返回模块的view,所有的模块视图需要实现该方法,并返回自己的view。然后,在activity中维护一份模块视图提供者列表(假设字段名为viewProviders),所有的模块在该列表中注册。最后,activity只需要遍历viewProviders,然后调用createView方法即可实现实现各个模块视图的获取。获取模块视图后再动态添加至activity主视图中即可。
这种实现,对于模块的增添改删都具有很强的扩展性,比如修改宫格模块数据的时候,只需要找到宫格模块视图的provider,进行修改即可。增加也同样,新增模块视图provider,实现自己的createView即可。
这种设计也更加遵循了面向对象的编程思维。
2、谁来决定视图的展示与否?
这里是说如果后端配置隐藏某个模块,那么这个模块在哪个时机进行隐藏?答案当然是要在具体的ViewProvider中的createView中进行,createView在网络数据返回后会拿到对应的配置数据,首先要对该数据进行解析,然后再判断是否配置为当前模块需要隐藏,如果是,则直接在createView中返回null即可,而此时acitivity添加模块视图的时候会根据createView是否为null来决定是否添加。
3、layout优化
1中提到解耦了activity,自然而然也为layout的解耦提供了方便。既然模块拥有独立的ViewProvider,且在ViewProvider中提供了自己的视图(createView),那么就将每个模块的layout也独立化,同时也不需要在activity的主layout中进行组装。那么每个模块视图如何加载?答案就是LayoutInflater,没错,通过这种加载方式即可实现。
既然视图可以模块自己提供,那么监听事件自然而然可以在其内部消化,这样也大大缩减了activity的代码。
4、有些模块不需要添加到view中,而是作为浮层展示怎么处理?
这个确实也是很常见的需求。本项目中典型的体现就是活动对话框的展示。该对话框并不是随时展示,而是后端配置的时候就展示,不配置就不展示。显然这个不是嵌入到activity的主view中的,而是作为浮层出现的,前面提到createView都会返回一个view,然后activity动态添加该view,那么对于不需要添加的dialog浮层该怎么处理呢?
答案依然是提供自己的ViewProvider,依然实现createView,所有的视图更新、事件处理都在自己的createview中实现,只不过这次返回值直接置为null即可,在activity中依然主动调用createView进行视图初始化,正如上面提到的,是否要添加到主视图,只需要依据其返回值是不是为null即可,为null不添加,不为null添加即可。
5、形态各异的分割线
这里的分割线包含两类,一类是模块内部的分割线;一类是模块结束后和另一个模块的分割线,这两个通常高度和样式不一样,而且即便是内部的分割线也不见得会一样,那么怎么提供这些分割线呢?难道要根据ui上设计好的分割线一个个添加到layout中吗?
当然不必,这种方法固然能实现,但显然很愚笨,不能更好的实现复用。更好的方案就是提供一个DivideLineProvider,该provider依然实现ViewProvider接口,然后在createView中实现分割线需求,而分割线的样式则可以暴露出修改诸如height、margin等方法,这样在模块使用的时候,也只需要设置分割线需要的样式,然后调用DivideLineProvider的createView添加到自己的视图中即可。视图模块内部可以这么添加,但是acitivity中的分割线怎么添加?是要在每个ViewProvider的后面都添加上DivideLineProvider吗?这个确实可以实现,只不过将DivideLineProvider作为一个普通的、和诸如宫格视图provider、banner视图provider一样的视图提供者了。但是这个有弊端,首先这个是没有业务意义的模块,没必要和业务模块混合在一起,其次一次增加这么多个provider显然也不合适,那么怎么解决呢?
答案就是将DivideLineProvider作为业务模块内部的一部分存在,在公共的模块视图提供接口ViewProvider中提供一个尾部分割视图实现方法比如tailDivideLine(),有各个模块视图的provider来进行实现。这样在activity中即可通过抽象的tailDivideLine()调用即可。与此同时也屏蔽了实现细节。
五、还能优化吗?
上个部分的优惠方案已经大大解耦了整个实现,尤其是对于activity,但是还不够,因为activity中维护了一个视图模块提供者列表,每个模块必须在这里面进行注册,意思是我增加一个模块还是要去activity中去注册该模块的ViewProvider,这个显然不合适。而且acitivity也没有必要维护这么一个列表,违背了设计的开闭原则和单一职责。
所以这里需要将这个列表抽出到一个单独的类中,这个类没有任何业务意义,只是用于充当视图配置的角色。以后新增视图到这里配置即可,这样任何视图的增删改查都不再需要改变activity中的实现代码了,至此更完美的方案实现完成。
所谓天外有天,人外有人,一定会有更优秀的实现方案,这里只是阐述了一种方案,欢迎讨论。