Android极简MVVM,从一个基类库谈起

语言: CN / TW / HK

highlight: a11y-dark

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

Hello啊各位老铁,今天带来一个老生常谈的技术,MVVM,这篇文章,主要详细介绍如何封装一个MVVM的基类库,以及MVVM架构模式在实际业务中的用法,最后会把实际的封装代码开源,并提供远程依赖,方便给到大家使用以及二次修改,尽量做到细致入微,浅显易懂,OK,废话不多赘述,我们进入正文。

这篇文章大概会按照以下几个模块进行阐述,此次封装,做到绝无第三方依赖,都是Android原生的代码封装,请放心使用,如果您想直接进行使用,请直接跳到第4步,集成使用即可,此次的封装,和目前主流的MVVM架构模式,会完美契合,让架构模式简单化,让业务代码清晰化,必须值得推荐使用。

一、MVVM简单概括

二、基于MVVM模式如何封装基类库

三、实战封装

四、封装后在业务中如何使用

五、开源以及Demo查看

温馨提示:内容稍多,请合理安排好时间,如果不想查阅具体封装过程,底部有开源地址,可以直接查看。

一、MVVM简单概括

MVVM的开发模式,相对来说低耦合,业务之间逻辑显得也十分分明,Model层负责将请求的数据交给ViewModel层;ViewModel层负责将请求到的数据做业务逻辑处理,最后交给View层去展示,与View一一对应;View层只负责界面绘制刷新,不处理业务逻辑,非常适合进行独立模块开发。

image.png

三层简单概括

1、Model:数据层,包含数据实体和对数据实体的操作。

2、View:视图层,对应于Activity,XML,View,负责数据显示以及用户交互。

3、ViewModel:关联层,将Model和View进行绑定,Model或者View更改时,实时刷新对方。

需要注意:

1、View只做和UI相关的工作,不涉及任何业务逻辑,不涉及操作数据,不处理数据,也就是UI和数据是严格分开的。

2、ViewModel只做和业务逻辑相关的工作,不涉及任何和UI相关的操作,不持有控件引用,不更新UI。

二、基于MVVM模式如何封装基类库

MVVM我们已经清晰,然而针对现有的三层,我们如何进行拆解封装呢?面对这样的一个问题,我们也是需要从三层以及和实际的业务进行相结合,从实际业务中来,也要从实际业务中去,这是我们封装的一个潜在因素,一旦脱离了实际,封装的再优秀,也只是一个花瓶,中看不中用。

针对MVVM中的三层,其实,我们在封装中,也是基于这三层,View,ViewModel和Model。View中,在实际的开发中,一般针对Activity和Fragment进行系统的抽取封装,ViewModel一般会抽取一个父类,做一些公共的方法或属性配置,Model层一般封装的较少,根据实际业务,需要具体问题具体分析。

Activity和Fragment的封装思路,其实是一致的,需要以简单和复杂两种方向进行抽取,一种是简单的页面继承使用,一种是复杂的页面继承使用,这样区分的一个目的,就是,专职专用,避免大材小用,而具体的封装,除了使得代码简洁化,更重要的拓展化,方便子类的调用。

在具体封装的时候,与实际业务相结合,这个无比重要,比如实际的大部分页面,都带有一个标题栏,那么标题栏就可以直接封装父类里面,像子类拓展出,更改标题,右侧按钮,左侧按钮等功能属性;除了统一的标题栏,另外就是子类的视图了,关于子类的视图传递,这个是必须的,可以直接抽象出一个必须要实现的方法,其他的,比如状态栏的改变,缺省页的设置等等,也需要在父类中统一的给出。

复杂的页面是基于简单的页面而来的,这里的复杂,一般是包含很多逻辑的处理,那么,我们就可以增加ViewModel层和Model层了,目前基于DataBinding的实现方式,无论简单和复杂,都是必须需要考虑的,也就是说在父类中,我们就需要向子类提供出可以拿到的databinding和viewmodel,一般以泛型的方式引入,这样子类再继承的时候,就可以很方便的进行调用。

在复杂的页面,也就是包含ViewModel层和Model层的时候,需要考虑绑定视图variable的传递,也就是当前的ViewModel和那个xml进行绑定,当然这是在需要的时候,必须要操作的,除了视图绑定,常见的,数据请求状态,比如请求成功,请求失败,缺省页显示和隐藏,Dialog的显示和隐藏,LiveData的数据回传等等,在复杂的页面中也是需要我们考虑的,除此之外,ViewModel中如何和View层的生命周期绑定,在实际的业务中也是不得不需要考虑的。

除了以上的常规考虑,在实际的业务中,比如事件消息传递,PagerAdapter使用,状态栏透明等很多和基类的相关的功能,我们其实也可以进行封装进去,便于子类的调用。

三、实战封装

通过第2条中的拆解和具体的封装思路,不妨我们进行实战一下,由于Activity和Fragment的封装思路以及相关属性和方法,大部分都是雷同的,所以目前只介绍Activity,更详细的封装,还请大家参考源码。

1、Activity的简单封装

简单封装,不携带ViewModel,只传递ViewDataBinding,子类必须重写的方法只有一个initData,其他均为选择性重写,如果相对逻辑比较简单的页面,可以继承此类。

一个很简单的普通封装,就是把共有的常见的,封装到父类里,便于子类的调用,具体什么方法,什么逻辑进行采取封装,需要我们根据具体业务或者公司的相关情况而定,以下是源码。

```kotlin abstract class BaseActivity(@LayoutRes layoutId: Int = 0) : AppCompatActivity(layoutId) {

private var mActionBarView: ActionBarView? = null
private var mLayoutError: LinearLayout? = null
private var mLayoutId = layoutId
lateinit var mBinding: VB


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    try {
        //默认状态栏为白底黑字
        darkMode(BaseConfig.statusBarDarkMode)
        statusBarColor(ContextCompat.getColor(this, BaseConfig.statusBarColor))
        setContentView(R.layout.activity_base)
        val baseChild = findViewById<LinearLayout>(R.id.layout_base_child)
        mLayoutError = findViewById(R.id.layout_empty_or_error)
        mActionBarView = findViewById(R.id.action_bar)
        if (mLayoutId == 0) {
            mLayoutId = getLayoutId()
        }


        if (savedInstanceState != null && getIntercept()) {
            noEmptyBundle()
            return
        }


        val childView = layoutInflater.inflate(mLayoutId, null)
        baseChild.addView(childView)
        mBinding = DataBindingUtil.bind(childView)!!


        initView()
        initData()
    } catch (e: Exception) {
        e.printStackTrace()
        noEmptyBundle()
    }
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取视图id
 */
open fun getLayoutId(): Int {
    return 0
}


open fun initView() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化数据
 */
abstract fun initData()




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:动态改变状态栏颜色和标题
 */
fun setDarkTitle(dark: Boolean, color: Int, title: String) {
    try {
        darkMode(dark)
        statusBarColor(ContextCompat.getColor(this, color))
        setBarTitle(title)


    } catch (e: Exception) {
        e.printStackTrace()
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:设置标题
 */
fun setBarTitle(title: String) {
    mActionBarView!!.visibility = View.VISIBLE
    mActionBarView!!.setBarTitle(title)
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隐藏左侧按钮
 */


fun hintLeftMenu() {
    mActionBarView!!.hintLeftBack()
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取ActionBarView
 */
fun getActionBarView(): ActionBarView {
    return mActionBarView!!
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隐藏标题栏
 */
fun hintActionBar() {
    mActionBarView?.visibility = View.GONE
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:Bundle为空进行拦截,解决改变权限后重回App崩溃问题
 */
open fun getIntercept(): Boolean {
    return false
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:Bundle为空时的逻辑处理,解决改变权限后重回App崩溃问题
 */
open fun noEmptyBundle() {}




override fun onDestroy() {
    super.onDestroy()
    try {
        LiveDataBus.removeObserve(this)
        LiveDataBus.removeStickyObserver(this)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:透明状态栏
 */
fun translucentWindow(dark: Boolean) {
    try {
        immersive(0, dark)
    } catch (e: Exception) {
        e.printStackTrace()
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:设置缺省页
 */
fun setEmptyOrError(view: View) {
    mLayoutError?.visibility = View.VISIBLE
    mLayoutError?.removeAllViews()
    mLayoutError?.addView(view)
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隐藏
 */
fun hintEmptyOrErrorView() {
    mLayoutError?.visibility = View.GONE
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取错误或为空的view
 */
fun getEmptyOrErrorView(): LinearLayout {
    return mLayoutError!!
}

} ```

涉及的方法概述

| 方法名 | 参数 | 概述 | | --- | --- | --- | | getLayoutId | 无参 | 子类传递的layout,用于加载视图,可以通过构造方法传递,也可以通过此方法传递。 | | initView | 无参 | 初始化View,非必须重写 | | initData | 无参 | 初始化数据 | | setDarkTitle | dark: Boolean, color: Int, title: String,1、dark: Boolean,状态栏颜色,true就是黑色,false就是白色。2、color: Int,状态栏背景颜色,3、title: String,标题栏内容 | 设置标题,状态栏背景及颜色 | | setBarTitle | title: String,标题栏内容 | 设置标题 | | hintLeftMenu | 无参 | 隐藏左侧按钮 | | getActionBarView | 无参 | 获取标题栏View,可以操作标题栏里的任何控件 | | hintActionBar | 无参 | 隐藏标题栏 | | translucentWindow | dark: Boolean,状态栏颜色,true就是黑色,false就是白色 | 透明状态栏 | | setEmptyOrError | view: View,传递的缺省View视图 | 设置缺省视图 | | hintEmptyOrErrorView | 无参 | 隐藏缺省视图 | | getEmptyOrErrorView | 无参 | 获取缺省视图 |

简单的Activity没有什么好说的,都是中规中矩,具体的使用请大家看第四条,具体使用即可。

2、Activity的复杂封装

也谈不上复杂,只是在继承简单页面的基础之上多加了一个ViewModel,相对于比较复杂的页面,就可以继承此类,此类,拓展了ViewModel,可以在ViewModel里进行逻辑的书写,此类也是MVVM的标准执行,V继承于BaseVMActivity,VM继承于BaseViewModel,至于M,可以在VM中通过getRepository方法进行获取。

具体代码逻辑如下:

BaseVMActivity继承于BaseActivity。

```kotlin abstract class BaseVMActivity(@LayoutRes layoutId: Int = 0) : BaseActivity(layoutId) {

lateinit var mViewModel: BM


override fun initData() {
    mViewModel = getViewModel()!!
    val variableId = getVariableId()
    if (variableId != -1) {
        mBinding.setVariable(getVariableId(), mViewModel)
        mBinding.executePendingBindings()
    }
    initVMData()
    observeLiveData()
    initState()
    lifecycle.addObserver(mViewModel)
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取绑定的xml id
 */
open fun getVariableId(): Int {
    return -1
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化状态
 */
private fun initState() {
    mViewModel.mStateViewLiveData.observe(this, {
        when (it) {
            StateLayoutEnum.DIALOG_LOADING -> {
                dialogLoading()
            }
            StateLayoutEnum.DIALOGD_DISMISS -> {
                dialogDismiss()
            }
            StateLayoutEnum.DATA_ERROR -> {
                dataError()
            }
            StateLayoutEnum.DATA_NULL -> {
                dataEmpty()
            }
            StateLayoutEnum.NET_ERROR -> {
                netError()
            }
            StateLayoutEnum.HIDE -> {
                hide()
            }
        }
    })
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化数据
 */
abstract fun initVMData()


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:LiveData的Observer
 */
open fun observeLiveData() {


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:dialog加载
 */
open fun dialogLoading() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:dialog隐藏
 */
open fun dialogDismiss() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:数据错误
 */
open fun dataError() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:数据为空
 */
open fun dataEmpty() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:网络错误或请求错误
 */
open fun netError() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隐藏某些布局或者缺省页等
 */
open fun hide() {}


private fun getViewModel(): BM? {
    //这里获得到的是类的泛型的类型
    val type = javaClass.genericSuperclass
    if (type != null && type is ParameterizedType) {
        val actualTypeArguments = type.actualTypeArguments
        val tClass = actualTypeArguments[1]
        return ViewModelProvider(
            this,
            ViewModelProvider.AndroidViewModelFactory.getInstance(application)
        )
            .get(tClass as Class<BM>)
    }
    return null
}

override fun onDestroy() { super.onDestroy() try { lifecycle.removeObserver(mViewModel) } catch (e: Exception) { e.printStackTrace() } }

} ```

封装涉及的方法概述

| 方法名 | 参数 | 概述| | --- | --- | --- | | getVariableId | 无参 | 获取绑定的xml variable,也就是当前的xml和哪个对象进行绑定,用于xml里直接数据绑定 | | initVMData | 无参 | 初始化数据,必须要实现的方法 | | observeLiveData | 无参 | LiveData的Observer,UI层监听ViewModel层的数据改变 | | dialogLoading | 无参 | dialog加载 | | dialogDismiss | 无参 | dialog隐藏 | | dataError | 无参 | 数据错误 | | dataEmpty | 无参 | 数据为空 | | netError | 无参 | 数据错误 | | hide | 无参 | 隐藏缺省页等其他页面 |

BaseViewModel

BaseViewModel相对比较简单,只提供了一个可以获取Repository的方法,还有一个是刷新UI视图的一个LiveData,就是数据请求,Dialog加载,缺省页加载的状态。更改状态,只需要调用changeStateView方法即可,子类可以重写生命周期方法,便于生命周期的考虑。

```kotlin

open class BaseViewModel : ViewModel() , BaseObserver{ /* * 控制状态视图的LiveData / val mStateViewLiveData = MutableLiveData()

/**
 * 更改状态视图的状态
 */
public fun changeStateView(
    state: StateLayoutEnum
) {
    // 对参数进行校验
    when (state) {
        StateLayoutEnum.DIALOG_LOADING -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DIALOG_LOADING)
        }
        StateLayoutEnum.DIALOGD_DISMISS -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DIALOGD_DISMISS)
        }
        StateLayoutEnum.DATA_ERROR -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DATA_ERROR)
        }
        StateLayoutEnum.DATA_NULL -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DATA_NULL)
        }
        StateLayoutEnum.NET_ERROR -> {
            mStateViewLiveData.postValue(StateLayoutEnum.NET_ERROR)
        }
        StateLayoutEnum.HIDE -> {
            mStateViewLiveData.postValue(StateLayoutEnum.HIDE)
        }
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取Repository
 */
inline fun <reified R> getRepository(): R? {
    try {
        val clazz = R::class.java
        return clazz.newInstance()
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期初始化 / override fun onCreate() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期页面可见 / override fun onStart() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期页面获取焦点 / override fun onResume() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期页面失去焦点 / override fun onPause() {

}

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期页面不可见 / override fun onStop() {

}

/* * AUTHOR:AbnerMing * INTRODUCE:生命周期页面销毁 / override fun onDestroy() { }

} ``` 复杂的Activity,大家可以发现,其实就是标准的MVVM形式封装,Fragment的封装也是基于此,搞清楚上述,基本上我们这个基类库就完成了大半,确实也没什么好说的,大家直接看使用吧。

四、封装后在业务中如何使用

通过以上的封装,我们在业务层所有的页面就可以继承父类进行使用,以达到代码的高度统一,使得架构模式简单化,让业务代码清晰化,目前的封装,大家可以直接封装成库或者打成aar给到其他开发者使用,目前我已经上传到远程,不想麻烦的老铁,可以直接按照下面的步骤进行使用。

1、在你的根项目下的build.gradle文件下,引入maven。

```

allprojects { repositories { maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" } } } ```

2、在你需要使用的Module中build.gradle文件下,引入依赖。

```

dependencies { implementation 'com.vip:base:1.0.2' } ```

通过以上的Maven仓库依赖,我们就可以愉快的进行使用了,下面针对各个封装的功能进行一个简单的演示,当然,大家可以直接看源码中的实例,那里相对比较全面。

1、普通的Activity的继承

如果,你的Activity页面逻辑比较简单,建议继承BaseActivity,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。

kotlin class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) { /** * AUTHOR:AbnerMing * INTRODUCE:初始化数据 */ override fun initData() { setBarTitle("主页") } }

2、ViewModel形式Activity的继承

View层,需要继承BaseVMActivity

```kotlin class TestViewModelActivity : BaseVMActivity(R.layout.activity_view_model) {

override fun initVMData() {
    setBarTitle("ViewModel方式使用")
}

} ```

ViewModel层,需要继承BaseViewModel

实际的业务中,遇到网络请求,缺省页展示,Dialog显示隐藏,调用changeStateView方法,UI层只需要重写对应的方法即可。

```kotlin

class TestViewModel : BaseViewModel() {

  /**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取需要的Repository
 */
private val repository by lazy {
    getRepository<TestRepository>()
}

} ``` Model层,一般根据实际需要,进行具体的封装使用。

```kotlin

class TestRepository {

} ```

3、DataBinding形式使用

View层,继承BaseVMActivity,返回当前视图的绑定variable

```kotlin

class DataBindActivity : BaseVMActivity(R.layout.activity_data_bind) {

override fun initVMData() {
    setBarTitle("DataBinding使用")
}


override fun getVariableId(): Int {
    return BR.data
}

} ```

ViewModel层继承BaseViewModel

```kotlin

class DataBindViewModel : BaseViewModel() {

var oneWayContent = "单向绑定数据测试"


var twoWayContent = "双向绑定数据测试"


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取双向绑定数据
 */
var clickListener = View.OnClickListener {


    Toast.makeText(it.context, twoWayContent, Toast.LENGTH_SHORT).show()
}

} ```

XML视图,直接绑定

```xml

<data>


    <variable
        name="data"
        type="com.abner.base.bind.DataBindViewModel" />
</data>


<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/gwm_dp_20"
    android:paddingRight="@dimen/gwm_dp_20">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:text="@{data.oneWayContent}" />


    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:hint="双向绑定"
        android:text="@={data.twoWayContent}" />


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:onClick="@{data.clickListener}"
        android:text="获取双向绑定数据" />


</LinearLayout>

```

4、Fragment的简单使用

如果,你的Fragment页面逻辑比较简单,建议继承BaseFragment,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。

```kotlin class TestPagerFragment : BaseFragment (R.layout.fragment_test_pager) {

override fun initData() {


}

} ```

5、ViewModel形式Fragment的继承

View层,需要继承BaseVMFragment

```kotlin class TestViewModelPagerFragment : BaseVMFragment(R.layout.fragment_test_pager) {

override fun initVMData() {

}

} ``` ViewModel层,需要继承BaseViewModel

```kotlin class TestFragmentViewModel :BaseViewModel(){

    /**
 * AUTHOR:AbnerMing
 * INTRODUCE:获取需要的Repository
 */
private val repository by lazy {
    getRepository<TestRepository>()
}

} ``` Model层,一般根据实际需要,进行具体的封装使用。

```kotlin

class TestRepository {

} ```

6、Fragment的DataBinding形式使用和Activity类似,就不赘述了。

7、事件消息总线使用

普通事件发送

kotlin LiveDataBus.send("send", "我发送了一条普通消息") 普通发送事件接收

```kotlin

LiveDataBus.observe(this, "send", Observer { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }) ``` 粘性事件发送

```kotlin

LiveDataBus.sendSticky("sendSticky", "我发送了一条粘性事件消息") ``` 粘性事件接收

```kotlin

LiveDataBus.observeSticky(this, "sendSticky", Observer { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }) ``` 更多的其他功能使用,大家直接看Github即可,上边有比较清晰的介绍。

image.png

五、开源以及Demo查看

以上的封装,目前已经开源,大家可以下载查看源码,或者进行二次更改使用,地址是:

https://github.com/AbnerMing888/VipBase

相关Demo,大家可以down下项目,运行即可,这里简单贴张效果图:

image.png

目前的封装,没有过多的冗余代码,完全可以满足实际的业务需要,大家可以按照这种模式试验一番,遇到问题,可以多多交流,毕竟,技术是开放的,交流中才能不断的进步,当然了,需要结合自己的实际业务进行使用,毕竟项目中不应存在多个架构模式,MVC也好,MVP,MVVM,MVI也罢,无论使用哪种,适合的才是最好的。

好了,各位老铁,这篇文章就到这里,下篇文章《组件化开发,从未如此简单》正在撰写中,大家敬请期待!