Android进阶宝典—组合替代继承,减少Base类滥用
背景
先说一下背景,当接触了比较多的项目之后,其实会发现每一个项目都会封装BaseActivity、BaseFragment等等。其实初衷其实是好的。每一个Activity和Fragment都是很多模板代码的,为了减少模板代码,封装进Base类其实是一种比较方便且可行的选择。
Base类涵盖了抽象、继承等面向对象特性,用得好会减少很多样板代码,但是一旦滥用,会对项目有很多弊端。
举个例子
当项目大了,需要封装进Base类的逻辑会非常多,比如说打印生命周期、ViewBinding 或者DataBinding封装、埋点、监听广播、监听EventBus、展示加载界面、弹Dialog等等其他业务逻辑,更有甚者把需要Context的函数都封装进Base类中。
以下举一个BaseActivity的例子,里面封装了上面所说的大部分情况,实际情况可能更多。
abstract class BaseActivity<T: ViewBinding, VM: ViewModel>: AppCompatActivity {protected lateinit var viewBinding: Tprotected lateinit var viewModel: VMoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 打印日志!!ELog.debugLifeCycle("${this.localClassName} - onCreate")// 初始化viewModelviewModel = initViewModel()// 初始化视图!!initView()// 初始化数据!!initData()// 注册广播监听!!registerReceiver()// 注册EventBus事件监听!!registerEventBus()// 省略一堆业务逻辑!// 设置导航栏颜色!!window.navigationBarColor = ContextCompat.getColor(this, R.color.primary_color)}protected fun initViewModel(): VM {// 初始化viewModel}private fun initViewbinding() {// 初始化viewBinding}// 让子类必须实现abstract fun initView()abstract fun initData()private fun registerReceiver() {// 注册广播监听}private fun unregisterReceiver() {// 注销广播监听}private fun registerEventBus() {// 注册EventBus事件监听}protected fun showDialog() {// 需要用到Context,因此也封装进来了}override fun onResume() {super.onResume()ELog.debugLifeCycle("${this.localClassName} - onResume")}override fun onPause() {super.onPause()ELog.debugLifeCycle("${this.localClassName} - onPause")}override fun onDestroy() {super.onDestroy()ELog.debugLifeCycle("${this.localClassName} - onDestroy")unregisterReceiver()}
}
其实看起来还好,但是在使用的时候难免会遇到一些问题,对于中途接手项目的人来说问题更加明显。我们从中途接手项目的心路历程看看Base类的缺陷。
心路历程
更多Android进阶学习资料 请点击免费领取
-
当创建新的Activity或者Fragment的时候需要想想有没有逻辑可以复用,就去找Base类,或许写Base类的人不同,发现一个项目中可能会存在多个Base类,甚至Base类仍然有多个Base子类实现不同逻辑。这个时候就需要去查看分析每个Base类分别实现了什么功能,决定继承哪个。
-
如果一个项目中只有一个Base类的话,仍需要看看Base类实现了什么逻辑,没有实现什么逻辑,防止重复写样板代码。
-
当出现Base类实现了的,而自己本身并不想需要,例如不想监听广播或者不想用ViewModel,对于不想监听广播的情况就要特殊做适配,例如往Base类加标志位。对于不想用ViewModel但是由于泛型限制,还是只能传进去,不然没法继承。
-
当发现自己集成Base类出BUG了,就要考虑改子类还是改Base类,由于大量的类都集成了Base类,显然改Base类比较麻烦,于是改自己比较方便。
-
如果一个Activity中展示了多个Fragment,可能会有业务逻辑的重复,其实只需要一个就好了。
其实第一第二点还好,时间成本其实没有重复写样板代码那么高。但是第三点的话其实用标志位来决定Base类的功能哪个需要实现哪个不需要实现并不是一种优雅的方式,反而需要重写的东西多了几个。第四点归根到底就是Base类其实并不好维护。
爬坑
那么对于Base类怎样实践才比较优雅呢?在我看来组合替代继承其实是一种不错的思路。对于Kotlin first的Android项目来说,组合替代继承其实是比较容易的。以下仅代表个人想法,有不同意见可以交流一下。
成员变量委托
对于ViewModel、Handler、ViewBinding这些Base变量使用委托的方式是比较方便的。
对于ViewBinding
委托,使用起来其实是非常简单的,只需要一行代码即可。
// Activity
private val binding by viewBinding(ActivityMainBinding::inflate)
// Fragment
private val binding by viewBinding(FragmentMainBinding::bind)
复制代码
对于ViewModel
委托,官方库则提供了一个viewBindings
委托函数。
private val viewModel:HomeViewModel by viewModels()
复制代码
需要在Gradle中引入ktx库
implementation 'androidx.fragment:fragment-ktx:1.5.1'
implementation 'androidx.activity:activity-ktx:1.5.1'
而对于Base变量则尽量少封装在Base类中,需要使用可以使用委托,因为如果实例了没有使用其实是比较浪费内存资源的,尽量按需实例。
扩展方法
对于需要用到Context上下文的逻辑封装到Base类中其实是没有必要的,在Kotlin还没有流行的时候,如果说需要使用到Context的工具方法,使用起来其实是不太优雅的。
例如展示一个Dialog:
class DialogUtils {public static void showDialog(Activity activity, String title, String content) {// 逻辑}
}
使用起来就是这样:
class MyActivity : AppCompatActivity() {...fun initButton() {button.setOnClickListener {DialogUtils.showDialog(this, "title", "content")}}
}
使用起来可能就会有一些迷惑,第一个参数把自己传进去了,这对于展示Dialog的语义上是有些奇怪的。按理来说只需要传title和content就好了。
这个时候就会有人想着把这个封装到Base类中。
public abstract class BaseActivity extends AppCompatActivity {protected void showDialog(String title, String content) {// 这里就可以用Context了}
}
使用起来就是这样:
class MyActivity : AppCompatActivity() {...fun initButton() {button.setOnClickListener {showDialog("title", "content")}}
}
是不是感觉好很多了。但是写在Base类中在Java中比较好用,对于Kotlin则完全可以使用扩展函数语法糖来替代了,在使用的时候和定义在Base类是一样的。
fun Activity.showDialog(title: String, content: String) {// this就能获取到Activity实例
}class MyActivity : AppCompatActivity() {...fun initButton() {button.setOnClickListener {// 使用起来和定义在Base类其实是一样的showDialog("title", "content")}}
}
这也说明了,需要使用到Context上下文的函数其实不用在Base类中定义,直接定义在顶层就好了,可以减少Base类的逻辑。
注册监听器
对于注册监听器这种情况则需要分情况,监听器是需要根据生命周期来注册和取消注册的,防止内存泄漏。对于不是每个子类都需要的情况,有的人可能觉得提供一个标志位就好了,如果不需要的话让子类重写。如果定义成抽象方法则每个子类都要重写,如果不是抽象方法的话,子类可能就会忘记重写。在我看来获取生命周期其实是比较简单的事情。按需添加代码监听就好了。
那么什么情况需要封装在Base类中呢?
-
怕之后接手项目的人忘记写这部分代码,则可以写到Base类中,例如打印日志或者埋点。
-
而对于界面太多难以测试的功能,例如收到被服务器踢下线的消息跳到登录页面,这个可以写进Base类中,因为基本上每个类都需要监听这种消息。
总结
没有最优秀的架构,只有最适合的架构!对于Base类大家的看法都不一样,追求更少的工作量完成更多事情这个目的是统一的。而Base类一旦臃肿起来了会造成整个项目难以维护,因此对于Base类应该辩证看待,养成只有必要的逻辑才写在Base类中的习惯,feature类应该使用组合的方式来使用,这对于项目的可维护性和代码的可调试性是有好处的。