Android 可扩展视图设计
前言
问题
飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。
当时面临一个问题:如何优雅地扩展一个View的功能?
常用方案
对于View的功能扩展,一般有三条路可走:
- 一个自定义View的无限膨胀
- 多层自定义View
- 多重继承自定义View
但是,这三个方案都有问题:
- 一个自定义View,会完全没有可复用性,可维护性差
- 多层自定义View,会有过度绘制问题(增加了视图层级)
- 多重继承自定义View,会有耦合性问题,因为如果有N个功能自由组合,使用继承的方式来实现,最终自定义View的个数会是:C(N,1)+C(N,2)+...+C(N,N)C(N,1)+C(N,2)+...+C(N,N)
一个想法
我们知道,在软件设计中有一对非常重要的概念:is-a 和 has-a 。 简单理解,is-a表示继承关系,has-a是组合关系,而has-a要比is-a拥有更好的可扩展性。
那么在扩展视图功能的时候,是不是也可以用has-a(组合)代替常用的is-a(继承)?
答案是可以的,而且我们可以使用委托模式来实现它,委托模式天然适合这个工作:设计的出发点就是为has-a替代is-a提供解决方案的, 而Kotlin在语言层面对委托模式提供了非常优雅的支持,在这种场景下可以使用它的by接口委托 。
探索
概念定义
- Widget: 系统View / ViewGroup、自定义View / ViewGroup。
- WidgetPlus: 委托者。继承自Widget,并可通过register()的方式has some items。
- DelegateItem: 被委托者。接受来自WidgetPlus的委托,负责业务逻辑的具体实现。
- IDelegate: 被委托者接口。
不支持在 Docs 外粘贴 block
流程设计
无法复制加载中的内容
角色转换
在被委托接口IDelagate的“润滑”下,Widget、WidgetPlus和Item相互之间是可以做到无缝转换的
Widget -> WidgetPlus
- 简单描述:一个视图可以改造为功能可扩展的视图(可双向)
- 转换方法:实现IDelegate接口、支持item注册
Widget -> DelegateItem
- 简单描述:自定义视图可以被改造为一个功能项,供其它可扩展视图动态配置(可双向)
- 转换方法:自定义Widget移除对Widget的继承,实现IDelegate接口
WidgetPlus -> DelegateItem
- 简单描述:一个可扩展视图(本身带有一部分功能),可被改造为功能项(可双向)
- 转换方法:移除对Widget的继承,保留IDelegate接口的实现
无法复制加载中的内容
通信和调用
可扩展视图和扩展项应该支持双向通信:
WidgetPlus -> DelegateItem
- 这个比较简单,WidgetPlus会用组合的方式持有Item,在收到业务或系统的请求时,委托Item去执行具体的实现逻辑。
DelegateItem -> WidgetPlus
- 在Item初始化的时候,需要传入WidgetPlus的相关信息(widgetPlus、context、attrs、defStyleAttr、defStyleRes)
WidgetPlus跟Items拥有相同的API,需要设置调用原则:
- 所有公共方法,一律使用WidgetPlus对象来触发(无论是在外部代码还是Item内部)
- Item私有方法,使用Item对象来触发
竞争机制
一个WidgetPlus同时持有多个Item的时候,如果这些Item被委托实现了相同的方法,那么就会出现Item的内部竞争问题。这里,可以根据方法类别来分别处理:
无返回值方法
- 比如
onMeasure()
,按照Item注册列表顺序执行
- 比如
有返回值方法
- 比如
onTouchEvent():Boolean
,这里出现了功能冲突,因为不可能同时返回多个值,只能取第一个返回值作为WidgetPlus的返回值。 - 对于这种情形,可以打印日志以便Develop时就被发现,解决方法有两种:
- 合而为一,即把两个Item合并,在一个Item中处理冲突;
- 分而治之,即把其中一个Item转换为WidgetPlus,创建两级视图。
关键点
1:1
- 一个WidgetPlus可以无限扩展Item功能项,但是对一种Item功能项只能持有一个对象。
- 但是,由于外部调用具有不可控性,所以register()的入参应该是Item的Class对象,在WidgetPlus内部反射调用Item的构造来生成对象。
Center
WidgetPlus中还是有一部分代码量的,为了减少Widget的转换成本、增加后续的可维护性,可以在WidgetPlus和Item直接再加一层DelegateCenter,由它来统一管理。
无法复制加载中的内容
Super
- 问题:在重写Widget的系统方法时,是需要执行superMethod的,而Item在进行业务实现时,无法直接触发到这个superMethod的。
- 有两个解决方案:
- 把Widget的method拆分为methodBefore()、methodAfter()、isHasSuper(),分别委托Item实现
- 把superMethod作为委托参数,这里可以使用Kotlin的方法类型参数
很显然,第二种方案要更好。
示意代码
/**
* Widget
*/
package android.widget;
public class LinearLayout extends ViewGroup {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}
}
/**
* WidgetPlus
*/
class LinearLayoutPlus() : LinearLayout(), IDelegate by DelegateCenter() {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
onDelegateMeasure(widthMeasureSpec, heightMeasureSpec) { _, _ ->
super.onMeasure(widthMeasureSpec, heightMeasureSpec)}
}
}
/**
* Center
*/
class DelegateCenter() : IDelegate {
private val itemList = mutableListOf<IItem>()
fun register(item: Class<IDelegate>) {
plusList.add(item.newInstance())
}
fun unRegister(item: Class<IDelegate>) {
plusList.remove(item)
}
override fun onDelegateMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int,
superMethod: (Int, Int) -> Unit) {
for (item in itemList) {
item.onDelegateMeasure(widthMeasureSpec, heightMeasureSpec,superMethod)
}
}
}
/**
* delegate interface
*/
interface IDelegate : IItem {
fun register(item: Class<IDelegate>)
fun unRegister(item: Class<IDelegate>)
}
/**
* Item interface
*/
interface IItem{
fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int,
superMethod: (Int, Int) -> Unit)
}
/**
* Item1
*/
class Item1() : IItem() {
override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: I nt, superMethod: (Int, Int) -> Unit) {}
}
/**
* Item2
*/
class Item2() : IItem() {
override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int, superMethod: (Int, Int) -> Unit) {}
}
/**
* main
*/
fun main() {
val plus = LinearLayoutPlus(context, attrs)
plus.register(Item1::class.java)
plus.register(Item2::class.java)
}
复制代码
背景知识
类与类之间的关系
- 类与类之间有六种关系:
关系 | 描述 | 耦合度 | 语义 | 代码层面 |
---|---|---|---|---|
继承 | 继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力 | ☆☆☆☆☆☆ | is-a | 在Java中继承关系通过关键字extends明确标识 |
实现 | 实现指的是一个类实现接口(可以是多个)的功能 | ☆☆☆☆☆ | is-a | 在Java中实现关系通过关键字implements明确标识 |
组合 | 它体现整体与部分间的关系,而且具有不可分割性,生命周期是一致的 | ☆☆☆☆ | contains-a | 类B作为类A的成员变量,只能从语义上来区别聚合和关联 |
聚合 | 它体现整体与部分间的关系,它们是可分离的,各有自己的生命周期 | ☆☆☆ | has-a | 类B作为类A的成员变量,只能从语义上来区别组合和关联 |
关联 | 这种使用关系具有长期性,而且双方的关系一般是平等的 | ☆☆ | has-a | 类B作为类A的成员变量,只能从语义上来区别组合和聚合 |
依赖 | 这种使用关系具有临时性,非常的脆弱 | ☆ | use-a | 类B作为入参,在类A的某个方法中被使用 |
- 继承和实现体现的一种纵向关系,一般是明确无异议的。而组合、聚合、关联和依赖体现的是横向关系,它们之间就比较难区分了,这几种关系都是语义级别的,从代码层面并不能完全区分。
委托模式
- 定义:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
- 能力: 是一种基础模式,状态模式、策略模式、访问者模式等在本质上就是在特殊场合采用了委托模式,委托模式使得我们可以用组合、聚合、关联来替代继承。
- 委托模式不能等价于代理模式: 虽然它们都是把业务需要实现的逻辑交给一个目标实现类来完成,但是使用代理模式的目的在于提供一种代理以控制对这个对象的访问,但是委托模式的出发点是将某个对象的请求拜托给另一个对象。
- 委托模式是可以自由切换被委托者,委托者甚至可以自实现业务逻辑,例如Java ClassLoader的双亲委派模型中,在委托父加载器加载失败的情况下,可以切换为自己去加载。