![06adfb7748258365bc09edc970babdbd.png](https://i-blog.csdnimg.cn/blog_migrate/6ebfd77cec7fa6b74c560c16ea7d06d1.jpeg)
一年前,用 Java 写了一个高可扩展选择按钮库。单个控件实现单选、多选、菜单选,且选择模式可动态扩展。
一年后,一个新的需求要用到这个库,项目代码已经全 Kotlin 化,强硬地插入一些 Java 代码显得格格不入,Java 冗余的语法也降低了代码的可读性,于是决定用 Kotlin 重构一番,在重构的时候也增加了一些新的功能。这一篇分享下重构的过程。
建议关注:不定时分享Android方面的技术、及大厂面试真题分析Android技术进阶屋zhuanlan.zhihu.com
![b7d611f0a6093ff7cd64690cc9c0161c.png](https://i-blog.csdnimg.cn/blog_migrate/d84a3029391d810e1ae90b5ee9433e23.jpeg)
选择按钮的可扩展性主要体现在 4 个方面:
- 选项按钮布局可扩展
- 选项按钮样式可扩展
- 选中样式可扩展
- 选择模式可扩展
扩展布局
原生的单选按钮通过RadioButton+ RadioGroup实现,他们在布局上必须是父子关系,而RadioGroup继承自LinearLayout,遂单选按钮只能是横向或纵向铺开,这限制的单选按钮布局的多样性,比如下面这种三角布局就难以用原生控件实现:
![228c91c8c990cdc8616417a2e13fe3be.gif](https://i-blog.csdnimg.cn/blog_migrate/7f9aa594f74b25c2bf155060b19b9fea.gif)
为了突破这个限制,单选按钮不再隶属于一个父控件,它们各自独立,可以在布局文件中任意排列,图中 Activity 的布局文件如下(伪码):
<
AgeSelector表示一个具体的按钮,本例中它是一个“上面是图片,下面是文字”的单选按钮。它继承自抽象的Selector。
扩展样式
从业务上讲,Selector长什么样是一个频繁的变化点,遂把“构建按钮样式”这个行为设计成Selector的抽象函数onCreateView(),供子类重写以实现扩展。
public
Selector继承自FrameLayout,实例化时会构建按钮视图,并把该视图作为孩子添加到自己的布局中。子类通过重写onCreateView()扩展按钮样式:
public
AgeSelector的样式被定义在 xml 中。
按钮被选中之后的样式,也是一个业务上的变化点,用同样的思路可以将Selector这样设计:
// 抽象按钮实现点击事件
将选中按钮状态变化的效果抽象成一个算法,延迟到子类实现:
public
AgeSelector在选中状态变化时定义了一个背景色渐变动画。
函数类型变量代替继承
在抽象按钮控件中,“按钮样式”和“按钮选中状态变换”被抽象成算法,算法的实现推迟到子类,用这样的方式,扩展按钮的样式和行为。
继承的一个后果就是类数量的膨胀,有没有什么办法不用继承就能扩展按钮样式和行为?
可以把构建按钮样式的成员方法onCreateView()设计成一个View类型的成员变量,通过设值函数就可以改变其值。但按钮选中状态变换是一种行为,在 Java 中行为的表达方式只有方法,所以只能通过继承来改变行为。
Kotlin 中有一种类型叫函数类型,运用这种类型,可以将行为保存在变量中:
class
选中样式和行为都被抽象为一个成员变量,只需赋值就可以动态扩展,不再需要继承:
// 构建按钮实例
在构建Selector实例的同时,指定了它的样式和选中变换效果(其中运用到 DSL 简化构建代码,详细介绍可以点击这里)
扩展选中模式
单个Selector已经可以很好的工作,但要让多个Selector形成一种单选或多选的模式,还需要一个管理器来同步它们之间的选中状态,Java 版本的管理器如下:
public
SelectorGroup将选中模式抽象成接口ChoiceAction,以便通过setChoiceMode()动态地扩展。
SelectorGroup还预定了两种选中模式:单选和多选。
单选可以理解为:点击按钮时,选中当前的并取消选中之前的。
多选可以理解为:点击按钮时无条件地反转当前选中状态。
Selector会持有SelectorGroup实例,以便将按钮点击事件传递给它统一管理:
public
然后就可以像这样实现单选:
SelectorGroup
也可以像这样实现菜单选:
SelectorGroup
将 Java 中的接口改成lambda,存储在函数类型的变量中,这样可省去注入函数,Kotlin 版本的SelectorGroup如下:
class
然后就可以像这样使用SelectorGroup:
// 构建管理器
构建的两个按钮拥有相同的groupTag和SelectorGroup,所以他们属于同一组并且是单选模式。
动态绑定数据
项目中一个按钮通常对应于一个“数据”,比如下图这种场景:
![4a98312ed7fd0060daed023f52a166c3.png](https://i-blog.csdnimg.cn/blog_migrate/a22b30d554b350d49ea63bb7c69542cb.png)
图中的分组数据和按钮数据都由服务器返回。点击创建组队时,希望在selectChangeListener中拿到每个选项的 ID。那如何为Selector绑定数据?
当然可以通过继承,在Selector子类中添加一个具体的业务数据类型来实现。但有没有更通用的方案?
ViewModel中设计了一种为其动态扩展属性的方法,将它应用在Selector中
class
为Selector新增一个Map类型的成员用于存放业务数据,业务数据被声明为Closeable的子类型,目的是将各式各样清理资源的行为抽象为close()方法,Selector重写了onDetachedFromWindow()且会遍历每个业务数据并调用它们的close(),即当它生命周期结束时,释放业务数据资源。
Selector也重载了设值和取值这两个运算符,以简化业访问业务数据的代码:
// 游戏属性实体类
因为重载了运算符,所以绑定和获取游戏属性的代码都更加简短。
用泛型就一定要强转?
绑定给 Selector 的数据被设计为泛型,业务层只有强转成具体类型才能使用,有什么办法可以不要在业务层强转?
CoroutineContext的键就携带了类型信息:
public
而且每一个CoroutineContext的具体子类型都对应一个静态的键实例:
public
这样,不需要强转就能获得具体子类型:
coroutineContext
模仿CoroutineContext,业务Selector的键设计了一个带泛型的接口:
interface
在为Selector绑定数据时需要先构建“键实例”:
val
传入的键带有类型信息,可以在取值方法中提前完成强转再返回给业务层使用:
// 值的具体类型被参数 key 指定,强转之后再返回给业务层
operator fun <T : Closeable> get(key: Key<T>): T? = (tags.getOrElse(key, { null })) as T
借助于 DSL 根据数据动态地构建选择按钮就变得很轻松,上一幅 Gif 展示的界面代码如下:
// 游戏属性集合实体类
这是两个 Demo 中用到的数据实体类,真实项目中他们应该是服务器返回的,简单起见,本地模拟一些数据:
val
最后用 DSL 动态构建选择按钮:
// 纵向布局
其中的按钮视图、按钮控制器、按钮效果变换器定义如下:
// 与游戏属性对应的键