[Android Climbing Diary 4] Combining alternative inheritance to reduce the abuse of Base class

background

Let's talk about the background first. When you come into contact with more projects, you will actually find that each project will encapsulate BaseActivity, BaseFragment, and so on. In fact, the original intention is actually good. Each Activity and Fragment has a lot of template code. In order to reduce the template code, it is actually a more convenient and feasible choice to encapsulate it into the Base class.

The Base class covers object-oriented features such as abstraction and inheritance. If used well, it will reduce a lot of boilerplate code, but if it is abused, it will have many drawbacks to the project.

for example

When the project is large, there will be a lot of logic that needs to be encapsulated into the Base class, such as printing life cycle, ViewBinding or DataBinding encapsulation, embedding, monitoring broadcast, monitoring EventBus, displaying loading interface, popping Dialog and other business logic, and more Even the functions that require Context are encapsulated into the Base class.

The following is an example of BaseActivity, which encapsulates most of the above-mentioned situations, and the actual situation may be more.

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()
    }
}
复制代码

In fact, it looks good, but it will inevitably encounter some problems when using it, and the problems are more obvious for those who take over the project in the middle. Let's take a look at the defects of the Base class from the process of taking over the project in the middle.

Mental journey

  1. When creating a new Activity or Fragment, you need to think about whether there is any logic that can be reused, and then go to the Base class. Maybe the people who write the Base class are different and find that there may be multiple Base classes in a project, and even the Base class still has Multiple Base subclasses implement different logic. At this time, you need to check and analyze what functions each Base class implements and decide which to inherit.

  2. If there is only one Base class in a project, you still need to see what logic the Base class implements and what logic it does not implement to prevent repetitive boilerplate code writing.

  3. When the Base class is implemented, but you don't want to need it, for example, you don't want to listen to the broadcast or don't want to use ViewModel, you need to make special adaptations if you don't want to listen to the broadcast, such as adding a flag to the Base class. For those who do not want to use ViewModel but due to generic restrictions, they can only be passed in, otherwise they cannot be inherited.

  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…

Guess you like

Origin juejin.im/post/7144671989159067656