在软件架构中暴露出的不同类(或组件)的相互引用总是会产生一定量的样板代码。
写大量的样板代码是一个体力活,大大降低了写代码的乐趣。想想刚入行的时候,能力有限,虽然想改善但是不知道怎么很好的解决,写公司项目变成了一件成就感很低的事情。
在 Android 中样板代码大致分为两类,一类是重复的成员初始化步骤,比如页面传参初始化从 Intent 中取值 ,ViewBinding ,ViewModel 初始化。另一类是固定的多个 API 出现逻辑上一致性的配置,比如 Adapter。
下面的实例表现了一种消除样板代码的直观效果:
// 简化前
class MyActivity : BaseActivity() {
private var userDesc: UserDesc? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.let {
userDesc = it.getParcelableExtra("userDesc")
}
}
}
// 简化后
class MyActivity : BaseActivity() {
@BundleParams("userId")
private int userId = -1;
override fun onCreate(xx) {}
}
通常我们会使用封装简化,或采用设计模式、注解&代码注入、插件自动生成代码来分别应对。这些方式都有一个特点,本身实现相对复杂,需要较多开发量才能完成,同时诸如注解处理还需要增加一定的编译时间。 比如 ButterKnife (过时)与 Hilt 等等。曾模仿写过 ButterKnife ,想要完整开发工作量可不小。
在面对更小的场景中,委托是一个不错的选择。下面将呈现采用委托完成 ViewBinding 初始化代码的简化。
下面是官网的 ViewBinding 的使用教程:
private lateinit var binding: ResultProfileBinding //1
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater) //2
val view = binding.root // 可省略
setContentView(view) //3
}
单独从完成初始化布局这个功能点上,这段代码没有任何问题。不难看出代码片段中的 1、2、3处代码就是样板代码。同时这里使用 (lateinit var binding)并不好,lateinit 是我很不喜欢的一个用法,它是一种逻辑上的妥协,如果使用前未做初始化判断就相当于埋下一颗暗雷。var 关键字导致 binding 是可变的,实际上 binding 作为不可变更为安全。
这时将委托引入我们可以提供下面这样的 API :
//BaseActivity
inline fun <reified VB : ViewBinding> lazyVB() = lazy(LazyThreadSafetyMode.PUBLICATION) {
ViewBindingHelper.getViewBindingInstanceByClass<VB>(
VB::class.java,
layoutInflater, null
)
}
abstract fun getContentVB(): ViewBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(state)
getContentVB()?.let { setContentView(it.root) }
}
//ChildActivity
private val binding by lazyVB<ResultProfileBinding>()
override fun getContentVB(): ViewBinding? = binding
这看起来似乎挺不错,binding 声明为不可变,并将在首次被访问的时候进行初始化,在 lazyVB 中借助反射创建出 binding 对象。但是又引入了一个新的样板代码方法 getContentVB,这很无奈,总需要有个地方调用 setContentView(?) 并传入 binding.root。消除因为 setContentView(?) 产生的 getContentVB 方法便是下个目标。
从源码可以看出委托本身也是一个类的实例,那么在类初始化时就完成生命周期关联自动完成ViewBinding 初始化与 setContentView(?) 是个办法。
依赖 Lazy 接口创建一个委托子类,Lazy 因为有委托的拓展所以直接继承它省事一些。构造参数接受 ComponentActivity 的实例用于获取 lifecycle 对象来关联生命周期。
class ActivityViewBindingDelegate<out T : ViewBinding>(
private val act: ComponentActivity,
private val initializer: () -> T
) : Lazy<T> {
private var _value: Any = UNINIT_VALUE
private val obs = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun setContentView() {
// 触发 value 的 get 方法
value
}
}
init {
act.lifecycle.addObserver(obs)
}
override val value: T
get() {
if (_value == UNINIT_VALUE) {
act.lifecycle.removeObserver(obs)
val vb = initializer()
act.setContentView(vb.root)
_value = vb
}
return _value as T
}
override fun isInitialized(): Boolean = _value !== UNINIT_VALUE
}
值得注意的是访问 binding 时虽然已经初始化但还未设置给 activity ,此时访问根 view 的 parent 将会得到空。 说实话那个单独的 value 访问,看起来有点滑稽🤪,这就是kotlin 可爱的地方吧。
修改 lazyVB 方法内的实现完成替换,这并不会影响上层调用。
inline fun <reified VB : ViewBinding> lazyVB() =
ActivityViewBindingDelegate(this) {
ViewBindingHelper.getViewBindingInstanceByClass<VB>(
VB::class.java,
layoutInflater, null
)
}
//ChildActivity
private val binding by lazyVB<ResultProfileBinding>()
嗯 ~,这样看起来很不错。
同样的针对 fragment 里对 ViewBinding 的初始化样板代码更多,不过依然能借助委托完成一模一样的效果。延展一下不难发现 ViewModel 也能完成这样的简化效果。结合起来在 Acitivty 与 Fragment 中页面初始化配置会像下面这样精简,只需聚焦业务逻辑即可。
private val vb by lazyVB<XBinding>()
private val vb by lazyVM<XViewModel>()
override fun initView(){
vb..
}
override fun bindData(){
vm..
}
在软件设计中有这样一个哲学问题,每引入一个新技术方案解决一个问题时,总会带来新的问题。尽管目前看起来不错,但我相信这个方案不是完美的,总会有需要改动的一天,那时候我应该还有兴趣改进。