[Android] How to write an elegant BaseActivity suitable for MVVM

How to write elegant BaseActivity in Android

  • 1 Introduction
  • 2. How to implement
  • 3. Actual development

1. Introduction

In daily development, I believe that we all have our own BaseActivity for Activity. So after introducing JetPack, how to make our BaseActivity simple and efficient has become a question worth thinking about.

  • Technology stack involved
    MVVM, JetPack, LiveData, DataBing, ViewModel
  • Let’s take a look at the final effect first


    Isn’t it very simple…

2. How to implement

  • MainActivity inherits from BaseActivityVBVM
abstract class BaseActivityVBVM<VB : ViewDataBinding, VM : ViewModel> : BaseActivityVB<VB>, IViewDataBinding<VB> {

    protected var _factory: ViewModelProvider.Factory?

    /**
     * For Hilt (@JvmOverloads kotlin default parameter value is invalid)
     * @constructor
     */
    constructor() : this(null)

    constructor(factory: ViewModelProvider.Factory?) : super(){
        _factory = factory
    }

    //

    protected lateinit var vm: VM

    //

    @CallSuper
    override fun initLayout() {
        super.initLayout()
        vm = UtilKViewModel.get(this, _factory/*, 1*/)
        bindViewVM(vb)
    }
}

vm is obtained through reflection

  • Look at BaseActivityVB again
abstract class BaseActivityVB<VB : ViewDataBinding>(
    /*protected var _factory: ViewModelProvider.Factory? = null*/
) : AppCompatActivity(), IActivity, IUtilK {

    protected val vb: VB by lazy(mode = LazyThreadSafetyMode.NONE) {
        UtilKViewDataBinding.get<VB>(this::class.java, layoutInflater/*, 0*/).apply {
            lifecycleOwner = this@BaseActivityVB
        }
    }

    ///

    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initFlag()
        initLayout()
        initData(savedInstanceState)
    }

    @CallSuper
    override fun onDestroy() {
        vb.unbind()
        super.onDestroy()
    }

    ///

    @CallSuper
    override fun initLayout() {
        setContentView(vb.root)
    }

    @CallSuper
    override fun initData(savedInstanceState: Bundle?) {
        initView(savedInstanceState)
        initObserver()
    }
}

Wait?! This is the end. Don’t be impatient. The best part is here.
Where did VB and VM come from –> Reflection

  • VM
 @JvmStatic
    @Suppress(CSuppress.UNCHECKED_CAST)
    fun <VB : ViewDataBinding> get(clazz: Class<*>, inflater: LayoutInflater/*, index: Int = 0*/): VB =
        UtilKReflectGenericKotlin.getParentGenericTypeByTClazz(clazz, ViewDataBinding::class.java)?.run {
            getDeclaredMethod("inflate", LayoutInflater::class.java).invoke(null, inflater) as VB
        } ?: throw Exception("inflate activity vb fail!")
  • Due to multi-level inheritance, we also need to consider generic type matching
 @JvmStatic
    fun getParentGenericTypeByTClazz(clazz: Class<*>, parentClazz: Class<*>/*, index: Int = 0*/): Class<*>? =
        getParentGenericTypeByT(clazz, parentClazz) as? Class<*>?

    @JvmStatic
    fun getParentGenericTypeByT(clazz: Class<*>, tClazz: Class<*>/*, index: Int = 0*/): Type? {
        val superClazz: Class<*>? = clazz.superclass
        val genericSuperclass: Type? = clazz.genericSuperclass
        if (genericSuperclass !is ParameterizedType) {//When the inherited class is not a parameterized type, look for it from the parent class
            return if (superClazz != null) {
                getParentGenericTypeByT(superClazz, tClazz)//Recursively search for generics when we inherit multi-layer BaseActivity
            } else
                null
        }
        genericSuperclass.actualTypeArguments.filterIsInstance<Class<*>>()
            .run {
                this.printlog()//for debug
                if (this.isNotEmpty()) {
                    for (clz in this) {
                        if (tClazz.isAssignableFrom(clz))
                            returnclz
                    }
                }
                if (superClazz != null)
                    return getParentGenericTypeByT(superClazz, tClazz)
                else
                    return null
            }
    }

3. Actual development

@AndroidEntryPoint
class MainActivity : BaseActivityVBVM<ActivityMainBinding, MainViewModel>() {

    override fun initView(savedInstanceState: Bundle?) {

    }
    override fun bindViewVM(vb: ActivityMainBinding) {
        vb.vm = vm
    }
}
@HiltViewModel
class MainViewModel @Inject constructor(private val _cache: Cache) : BaseViewModel() {
    var number_gege by VarProperty_Set(_cache.number_gege) { field, value ->
        lv_number_gege.value = value.toString()
        _cache.number_gege = value
        true
    }
    var number_meimei by VarProperty_Set(_cache.number_meimei) { field, value ->
        lv_number_meimei.value = value.toString()
        _cache.number_meimei = value
        true
    }
    val lv_number_gege = MutableLiveData(_cache.number_gege.toString())
    val lv_number_meimei = MutableLiveData(_cache.number_meimei.toString())

    fun add_gege(other: Double) {
        number_gege = number_gege.toBigDecimal().add(BigDecimal(other)).toDouble()
    }

    fun minus_gege(other: Double) {
        number_gege = number_gege.toBigDecimal().minus(BigDecimal(other)).toDouble()
    }

    fun add_meimei(other: Double) {
        number_meimei = number_meimei.toBigDecimal().add(BigDecimal(other)).toDouble()
    }

    fun minus_meimei(other: Double) {
        number_meimei = number_meimei.toBigDecimal().minus(BigDecimal(other)).toDouble()
    }
}

bingo This is all the code, wait, there are some that are not posted, don’t worry, the hot open source address is as follows
1. The open source address of the library in the picture: SwiftKit
1. The open source address of the example project in the picture: LoveCalculator