【Android爬坑日记】组合替代继承,减少Base类滥用

6,474 阅读6分钟

背景

先说一下背景,当接触了比较多的项目之后,其实会发现每一个项目都会封装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: T
    
    protected lateinit var viewModel: VM

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 打印日志!!
        ELog.debugLifeCycle("${this.localClassName} - onCreate")
       
        // 初始化viewModel
        viewModel = 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类的缺陷。

心路历程

  1. 当创建新的Activity或者Fragment的时候需要想想有没有逻辑可以复用,就去找Base类,或许写Base类的人不同,发现一个项目中可能会存在多个Base类,甚至Base类仍然有多个Base子类实现不同逻辑。这个时候就需要去查看分析每个Base类分别实现了什么功能,决定继承哪个。

  2. 如果一个项目中只有一个Base类的话,仍需要看看Base类实现了什么逻辑,没有实现什么逻辑,防止重复写样板代码。

  3. 当出现Base类实现了的,而自己本身并不想需要,例如不想监听广播或者不想用ViewModel,对于不想监听广播的情况就要特殊做适配,例如往Base类加标志位。对于不想用ViewModel但是由于泛型限制,还是只能传进去,不然没法继承。

  4. 当发现自己集成Base类出BUG了,就要考虑改子类还是改Base类,由于大量的类都集成了Base类,显然改Base类比较麻烦,于是改自己比较方便。

  5. 如果一个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类应该使用组合的方式来使用,这对于项目的可维护性和代码的可调试性是有好处的。

参考

juejin.cn/post/707789…