Android Jetpack之MVVM使用及封装
Android开发架构
如果开发过程中大家各自为战,没有统一规范,久而久之,项目代码会变得混乱且后续难以维护。当使用统一的架构模式后,有很多的好处,如:
- 统一开发规范,使得代码整洁、规范,后续易于维护及扩展
- 提高开发效率(尤其在团队人员较多时)
- 模块单一职责,使得模块专注自己内部(面向对象),模块间解耦
总之,开发架构是前人总结出来的一套行之有效的开发模式,目的是达到高内聚,低耦合的效果,使得项目代码更健壮、易维护。
Android中常见的架构模式有MVC(Model-View-Controller)
、MVP(Model-View-Presenter)
、MVVM(Model-View-ViewModel)
,一起来看下各自的特点:
MVC
MVC(Model-View-Controller)
是比较早期的架构模式,模式整体也比较简单。
MVC模式将程序分成了三个部分:
- Model模型层:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
- View视图层:页面视图(通过XML布局编写视图层),负责接收用户输入、发起数据请求及展示结果页面
- Controller控制器层:M与V之间的桥梁,负责业务逻辑
MVC特点:
- 简单易用:上图表述了数据整个流程:
View
接收用户操作,通过Controller
去处理业务逻辑,并通过Model
去获取/更新数据,然后Model层
又将最新的数据传回View
层进行页面展示。 - 架构简单的另一面往往是对应的副作用:由于XML布局能力弱,我们的View层的很多操作都是写在Activity/Fragment中,同时,Controller、Model层的代码也大都写在Activity/Fragment中,这就会导致一个问题,当业务逻辑比较复杂时,Activity/Fragment中的代码量会很大,其违背了类单一职责,不利于后续扩展及维护。尤其是后期你刚接手的项目,一个Activity/Fragment类中的代码动辄上千行代码,那感觉着实酸爽:
当然,如果业务很简单,使用MVC模式还是一种不错的选择。
MVP
MVP(Model-View-Presenter)
,架构图如下:
MVP各模块职责如下:
- Model模型:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
- View视图:页面视图(Activity/Fragment),负责接收用户输入、发起数据请求及展示结果页面
- Presenter:M与V之间的桥梁,负责业务逻辑
MVP特点: View
层接收用户操作,并通过持有的Presenter
去处理业务逻辑,请求数据;接着Presenter
层通过Model
去获取数据,然后Model
又将最新的数据传回Presenter
层,Presenter
层又持有View
层的引用,进而将数据传给View
层进行展示。
MVP相比MVC的几处变化:
- View层与Model层不再交互,而是通过Presenter去进行联系
- 本质上MVP是面向接口编程,Model/View/Presenter每层的职责分工明确,当业务复杂时,整个流程逻辑也是很清晰的
当然,MVP
也不是十全十美的,MVP
本身也存在以下问题:
View
层会抽象成IView
接口,并在IView
中声明一些列View
相关的方法;同样的,Presenter
会被抽象成IPresenter
接口及其一些列方法,每当实现一个功能时,都需要编写多个接口及其对应的方法,实现起来相对比较繁琐,而且每次有改动时,对应的接口方法也基本都会再去改动。View
层与Presenter
层相互持有,当View
层关闭时,由于Presenter
层不是生命周期感知的,可能会导致内存泄漏甚至是崩溃。
ps:如果你的项目中使用了RxJava
,可以使用 AutoDispose 自动解绑。
MVVM
MVVM(Model-View-ViewModel)
,架构图如下:
MVVM各职责如下:
- Model模型:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
- View视图:页面视图(Activity/Fragment),负责接收用户输入、发起数据请求及展示结果页面
- ViewModel:M与V之间的桥梁,负责业务逻辑
MVVM特点:
- View层接收用户操作,并通过持有的ViewModel去处理业务逻辑,请求数据;
- ViewModel层通过Model去获取数据,然后Model又将最新的数据传回ViewModel层,到这里,ViewModel与Presenter所做的事基本是一样的。但是ViewModel不会也不能持有View层的引用,而是View层会通过观察者模式监听ViewModel层的数据变化,当有新数据时,View层能自动收到新数据并刷新界面。
UI驱动 vs 数据驱动
MVP
中,Presenter
中需要持有View
层的引用,当数据变化时,需要主动调用View
层对应的方法将数据传过去并进行UI刷新,这种可以认为是UI驱动;而MVVM
中,ViewModel
并不会持有View
层的引用,View
层会监听数据变化,当ViewModel
中有数据更新时,View
层能直接拿到新数据并完成UI更新,这种可以认为是数据驱动,显然,MVVM
相比于MVP
来说更加解耦。
MVVM的具体实现
上面介绍了MVC/MVP/MVVM
的各自特点,其中MVC/MVP
的具体使用方式,本文不再展开实现,接下来主要聊一下MVVM
的使用及封装,MVVM
也是官方推荐的架构模式。
Jetpack MVVM
Jetpack
是官方推出的一系列组件库,使用组件库开发有很多好处,如:
- 遵循最佳做法:采用最新的设计方法构建,具有向后兼容性,可以减少崩溃和内存泄漏
- 消除样板代码:开发者可以更好地专注业务逻辑
- 减少不一致:可以在各种Android版本中运行,兼容性更好。
为了实现上面的MVVM
架构模式,Jetpack
提供了多个组件来实现,具体来说有Lifecycle、LiveData、ViewModel(这里的ViewModel是MVVM中ViewModel层的具体实现)
,其中Lifecycle
负责生命周期相关;LiveData
赋予类可观察,同时还是生命周期感知的(内部使用了Lifecycle
);ViewModel
旨在以注重生命周期的方式存储和管理界面相关的数据,针对这几个库的详细介绍及使用方式不再展开,有兴趣的可以参见前面的文章:
通过这几个库,就可以实现MVVM
了,官方也发布了MVVM
的架构图:
其中Activity/Fragment
为View
层,ViewModel+LiveData
为ViewModel
层,为了统一管理网络数据及本地数据数据,又引入了Repository
中间管理层,本质上是为了更好地管理数据,为了简单把他们统称为Model
层吧。
使用举例
- View层代码:
//MvvmExampleActivity.kt
class MvvmExampleActivity : BaseActivity() {
private val mTvContent: TextView by id(R.id.tv_content)
private val mBtnQuest: Button by id(R.id.btn_request)
private val mToolBar: Toolbar by id(R.id.toolbar)
override fun getLayoutId(): Int {
return R.layout.activity_wan_android
}
override fun initViews() {
initToolBar(mToolBar, "Jetpack MVVM", true)
}
override fun init() {
//获取ViewModel实例,注意这里不能直接new,因为ViewModel的生命周期比Activity长
mViewModel = ViewModelProvider(this).get(WanViewModel::class.java)
mBtnQuest.setOnClickListener {
//请求数据
mViewModel.getWanInfo()
}
//ViewModel中的LiveData注册观察者并监听数据变化
mViewModel.mWanLiveData.observe(this) { list ->
val builder = StringBuilder()
for (index in list.indices) {
//每条数据进行折行显示
if (index != list.size - 1) {
builder.append(list[index])
builder.append("\n\n")
} else {
builder.append(list[index])
}
}
mTvContent.text = builder.toString()
}
}
}
- ViewModel层代码:
//WanViewModel.kt
class WanViewModel : ViewModel() {
//LiveData
val mWanLiveData = MutableLiveData<List<WanModel>>()
//loading
val loadingLiveData = SingleLiveData<Boolean>()
//异常
val errorLiveData = SingleLiveData<String>()
//Repository中间层 管理所有数据来源 包括本地的及网络的
private val mWanRepo = WanRepository()
fun getWanInfo(wanId: String = "") {
//展示Loading
loadingLiveData.postValue(true)
viewModelScope.launch(Dispatchers.IO) {
try {
val result = mWanRepo.requestWanData(wanId)
when (result.state) {
State.Success -> mWanLiveData.postValue(result.data)
State.Error -> errorLiveData.postValue(result.msg)
}
} catch (e: Exception) {
error(e.message ?: "")
} finally {
loadingLiveData.postValue(false)
}
}
}
}
- Repository层(Model层)代码:
class WanRepository {
//请求网络数据
suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
val service = RetrofitUtil.getService(DrinkService::class.java)
val baseData = service.getBanner()
if (baseData.code == 0) {
//正确
baseData.state = State.Success
} else {
//错误
baseData.state = State.Error
}
return baseData
}
}
这里只通过Retrofit
请求了网络数据 玩Android 开放API,如果需要添加本地数据,只需要在方法里添加本地数据处理即可,即 Repository是数据的管理中间层,对数据进行统一管理,ViewModel层中不需要关心数据的来源,大家各司其职即可,符合单一职责,代码可读性更好,同时也更加解耦。在View
层点击按钮请求数据,执行结果如下:
以上就完成了一次网络请求,相比于MVP
,MVVM
既不用声明多个接口及方法,同时ViewModel
也不会像Presenter
那样去持有View
层的引用,而是生命周期感知的,MVVM
方式更加解耦。
封装
上一节介绍了Jetpack MVVM的使用例子,可以看到有一些代码逻辑是可以抽离出来封装到公共部分的,那么本节就尝试对其做一次封装。
首先,请求数据时可能会展示Loading
,请求完后可能是空数据、错误数据,对应下面的IStatusView
接口声明:
interface IStatusView {
fun showEmptyView() //空视图
fun showErrorView(errMsg: String) //错误视图
fun showLoadingView(isShow: Boolean) //展示Loading视图
}
因为ViewModel是在Activity中初始化的,所以可以封装成一个Base类:
abstract class BaseMvvmActivity<VM : BaseViewModel> : BaseActivity(), IStatusView {
protected lateinit var mViewModel: VM
protected lateinit var mView: View
private lateinit var mLoadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mLoadingDialog = LoadingDialog(this, false)
mViewModel = getViewModel()!!
init()
registerEvent()
}
/**
* 获取ViewModel 子类可以复写,自行初始化
*/
protected open fun getViewModel(): VM? {
//当前对象超类的Type
val type = javaClass.genericSuperclass
//ParameterizedType表示参数化的类型
if (type != null && type is ParameterizedType) {
//返回此类型实际类型参数的Type对象数组
val actualTypeArguments = type.actualTypeArguments
val tClass = actualTypeArguments[0]
return ViewModelProvider(this).get(tClass as Class<VM>)
}
return null
}
override fun showLoadingView(isShow: Boolean) {
if (isShow) {
mLoadingDialog.showDialog(this, false)
} else {
mLoadingDialog.dismissDialog()
}
}
override fun showEmptyView() {
......
}
//错误视图 并且可以重试
override fun showErrorView(errMsg: String) {
.......
}
private fun registerEvent() {
//接收错误信息
mViewModel.errorLiveData.observe(this) { errMsg ->
showErrorView(errMsg)
}
//接收Loading信息
mViewModel.loadingLiveData.observe(this, { isShow ->
showLoadingView(isShow)
})
}
abstract fun init()
}
Base
类中初始化ViewModel
,还可以通过官方activity-ktx
、fragment-ktx
扩展库,初始化方式:val model: VM by viewModels()
。
子类中继承如下:
class MvvmExampleActivity : BaseMvvmActivity<WanViewModel>() {
private val mTvContent: TextView by id(R.id.tv_content)
private val mBtnQuest: Button by id(R.id.btn_request)
private val mToolBar: Toolbar by id(R.id.toolbar)
override fun getLayoutId(): Int {
return R.layout.activity_wan_android
}
override fun initViews() {
initToolBar(mToolBar, "Jetpack MVVM", true)
}
override fun init() {
mBtnQuest.setOnClickListener {
//请求数据
mViewModel.getWanInfo()
}
/**
* 这里使用了扩展函数,等同于mViewModel.mWanLiveData.observe(this) {}
*/
observe(mViewModel.mWanLiveData) { list ->
val builder = StringBuilder()
for (index in list.indices) {
//每条数据进行折行显示
if (index != list.size - 1) {
builder.append(list[index])
builder.append("\n\n")
} else {
builder.append(list[index])
}
}
mTvContent.text = builder.toString()
}
}
}
我们把ViewModel的初始化放到了父类里进行,代码看上去更简单了。监听数据变化mViewModel.mWanLiveData.observe(this) {}
方式改成observe(mViewModel.mWanLiveData) {}
方式,少传了一个LifecycleOwner,其实这是一个扩展函数,如下:
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, observer: (t: T) -> Unit) {
liveData.observe(this, { observer(it) })
}
ps:我们初始化View控件时,如 private val mBtnQuest: Button by id(R.id.btn_request)
,依然使用了扩展函数,如下:
fun <T : View> Activity.id(id: Int) = lazy {
findViewById<T>(id)
}
不用像写java代码中那样时刻要想着判空,同时只会在使用时才会进行初始化,很实用!
说回来,接着是ViewModel层的封装,BaseViewModel.kt
:
abstract class BaseViewModel : ViewModel() {
//loading
val loadingLiveData = SingleLiveData<Boolean>()
//异常
val errorLiveData = SingleLiveData<String>()
/**
* @param request 正常逻辑
* @param error 异常处理
* @param showLoading 请求网络时是否展示Loading
*/
fun launchRequest(
showLoading: Boolean = true,
error: suspend (String) -> Unit = { errMsg ->
//默认异常处理,子类可以进行覆写
errorLiveData.postValue(errMsg)
}, request: suspend () -> Unit
) {
//是否展示Loading
if (showLoading) {
loadStart()
}
//使用viewModelScope.launch开启协程
viewModelScope.launch(Dispatchers.IO) {
try {
request()
} catch (e: Exception) {
error(e.message ?: "")
} finally {
if (showLoading) {
loadFinish()
}
}
}
}
private fun loadStart() {
loadingLiveData.postValue(true)
}
private fun loadFinish() {
loadingLiveData.postValue(false)
}
}
扩展一下: 1、上面执行网络请求时,使用viewModelScope.launch来启动协程,引入方式:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
这样就可以直接在ViewModel中启动协程并且当ViewModel生命周期结束时协程也会自动关闭,避免使用GlobalScope.launch { }
或MainScope().launch { }
还需自行关闭协程, 当然,如果是在Activity/Fragment、liveData中使用协程,也可以按需引入:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
具体可以参见官方的 将 Kotlin 协程与生命周期感知型组件一起使用 这篇文章。
2、另外细心的读者可能观察到,上面我们的Loading、Error信息监听都是用的SingleLiveData,把这个类打代码贴一下:
/**
* 多个观察者存在时,只有一个Observer能够收到数据更新
* https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
*/
class SingleLiveData<T> : MutableLiveData<T>() {
companion object {
private const val TAG = "SingleLiveEvent"
}
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { t ->
//如果expect为true,那么将值update为false,方法整体返回true,
//即当前Observer能够收到更新,后面如果还有订阅者,不能再收到更新通知了
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(@Nullable value: T?) {
//AtomicBoolean中设置的值设置为true
mPending.set(true)
super.setValue(value)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}
可以看到SingleLiveData还是继承自MutableLiveData,区别是当多个观察者存在时,只有一个Observer能够收到数据更新,本质上是在observe()时通过CAS加了限制,注释已经很详细了,不再赘述。
子类中继承如下:
class WanViewModel : BaseViewModel() {
//LiveData
val mWanLiveData = MutableLiveData<List<WanModel>>()
//Repository中间层 管理所有数据来源 包括本地的及网络的
private val mWanRepo = WanRepository()
fun getWanInfo(wanId: String = "") {
launchRequest {
val result = mWanRepo.requestWanData(wanId)
when (result.state) {
State.Success -> mWanLiveData.postValue(result.data)
State.Error -> errorLiveData.postValue(result.msg)
}
}
}
}
最后是对Model层的封装,BaseRepository.kt
:
open class BaseRepository {
suspend fun <T : Any> executeRequest(
block: suspend () -> BaseData<T>
): BaseData<T> {
val baseData = block.invoke()
if (baseData.code == 0) {
//正确
baseData.state = State.Success
} else {
//错误
baseData.state = State.Error
}
return baseData
}
}
数据基类BaseData.kt
:
class BaseData<T> {
@SerializedName("errorCode")
var code = -1
@SerializedName("errorMsg")
var msg: String? = null
var data: T? = null
var state: State = State.Error
}
enum class State {
Success, Error
}
子类中继承如下:
class WanRepository : BaseRepository() {
suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
val service = RetrofitUtil.getService(DrinkService::class.java)
return executeRequest {
service.getBanner()
}
}
}
到此,基本上就完成了。