一、问题
相信大家对 MVP 都比较熟悉了,先简单的回忆下 MVP,它的全称是 Model-View-Presenter,三部分的耦合关系如下:
从图中可以看出一个明显的问题,那就是P层和V层需要互相持有引用,理想的情况下,我们并不希望P层持有V层的引用,但由于一些原因我们必须这么做,例如将P层异步网络请求的数据返回给V层,这就必须让V层实现约定好的接口,然后在P层调用V层对象的接口方法来返回数据。这样就导致项目中可能会出现大量的接口定义,仅仅为了把P层的数据传到V层,个人感觉挺这样挺鸡肋的。
除此之外,这样的耦合也为单元测带来了困难。
抛开 MVP 耦合的问题先不谈,我们开发中可能会面对这样的问题,例如:网络请求必须用异步回调的方式去处理,但如果业务比较复杂,可能会出现所谓的“回调地狱”,那能不能用同步的方式实现呢?以及 Activity/Fragmentf 非正常的被销毁重新创建,例如横竖屏切换,如何保证数据不丢失?在网络请求或其它操作未正常返回前,如果页面被关闭,如何保证后续的处理不产生异常、避免内存泄漏?等等。当然,这些问题都有解决方案,但在接下来要学习的 MVVM 框架中,这些问题会被顺便的解决掉,而不用刻意的找各种方案。
二、方案
先认识下 MVVM,它的全称是 Model-View-ViewModel,三部分的耦合关系如下:
和 MVP 相比,在 MVVM 中 ViewModel 的作用类似于Presenter,简称VM层,但VM层并不会持有V层的引用,这样耦合的问题得到了解决。
那我们的 MVVM 架构具体如何实现呢?发现问题,找到改进方案是第一步,如何落地实现才是关键。
这里我们基于Kotlin语言实现,采用Kotlin的Coroutines(协程),以及Jetpack 中的ViewModel、LiveData、DataBinding 组件来实现 MVVM 整体框架的搭建,这些技术也是官方推荐的方案,某种程度上也代表 Android 技术的发展方向,还是值得我们去学习的。
上边提到了Kotlin的Coroutines(协程),Jetpack中的ViewModel、LiveData、DataBinding组件,首先对它们要有一定的了解。
1、ViewModel
ViewModel 可以在 Activity/Fragment 这些组件被短暂销毁的时候保存数据,例如横竖屏的切换等,然后在这些组件被重新创建时自动恢复数据,不需要开发者做额外的操作。我们一般会定义 ViewModel 的类,让它为 Activity/Fragment 提供数据支撑,而不是 Activity/Fragment 直接去做这些事,Activity/Fragment 会持有一个 ViewModel 对象,调用相关方法去获取数据 。这样可以将界面的数据显示、用户操作的响应等业务和数据的请求处理业务分开。
一般我们都会在 Activity/Fragment 中发起诸如网络请求的异步操作,由于异步操作的延时性,我们必须去维护管理这些异步操作,去避免由于 Activity/Fragment 关闭或切到后台导致的崩溃或者内存泄漏。但现在我们有了更好的方案,那就是 ViewModel + LiveData 的组合。
ViewModel 对象存活的时间范围和创建它时传递的 Activity/Fragment 的生命周期相关,从 Activity/Fragment 创建到最终销毁,由于横竖屏的切换导致 Activity/Fragment 被短暂销毁时,不会影响 ViewModel 对象。具体可参考:ViewModel 的生命周期
2、LiveData:
LiveData 是一种可观察的数据存储器类,但和一般的可观察类不同,LiveData 具有生命周期感知能力,它会遵循如 Activity/Fragment 等组件的生命周期。这样可以确保 LiveData 中保存的数据有变化时,只会通知处于活跃生命周期状态的应用组件观察者。
我们前边的 ViewModel 类可以通过 LiveData 来真正实现数据的存储,网络请求的数据、数据库操作的数据都可以交给 LiveData,所以我们以一般在 Activity/Fragment 里,会调用 LiveData 的observe()
方法和 LiveData 建立观察绑定关系,当 LiveData 中的数据变化时会通过主线程通知它的观察者,也就是 Activity/Fragment 去更新 UI。
由于 LiveData 具有生命周期感知能力,当 Activity/Fragment 处于非活跃状态时,就不会接收任何 LiveData 发送的任何事件通知,从而避免 Activity/Fragment 因处于非活跃状态时,去更新 UI 而发生崩溃。并且当 Activity/Fragment 被销毁后,LiveData 进会自行进行数据的清理、释放,避免内存泄漏发生。保证我们开发的应用更加的稳健。
更多关于 LiveData 的细节可以参考:LiveData
3、DataBinding
对于 DataBinding, 主要就是帮我们完成数据和控件之间的绑定工作,省去了我们主动获取控件然后去绑定数据的过程。在测试项目中有用到,但我个人感觉可以使用,也可以不使用,对于我们要搭建的 MVVM 框架并不是必须的。这里不做过多的说明,可以自行了解:DataBinding
4、Coroutines
协程的概念可能比较难理解,可以参考扔物线老师码上开学中的系列文章来学习。这里简单的总结下:
协程可以理解成 Kotlin 官方提供的一套多线程操作的 API,可以用看起来同步的方式写出异任务的代码,消除了异步任务的回调,可以在同一个协程中进行多次的线程切换。
当协程执行到一个挂起函数(suspend 关键字标记)时,协程会被挂起,即协程从正在执行它的线程上脱离,暂时不再被当前线程执行。之前执行协程的线程会继续处理后续任务;被挂起的协程会继续执行挂起函数,比如协程之前运行在主线程,挂起函数将协程切到一个子线程去执行异步任务,执行完成后会自动切回主线程,协程将被继续执行。注意被 suspend 标记的函数并直接挂起协程,它只是一个标记。
启动协程时需要指定 Coroutines Scope,即协程的范围,来管理协程, ViewModel 类扩展了一个 viewModelScope
对象。如果 ViewModel 被销毁,则在此范围内启动的协程都会自动取消。我们在 ViewModel 中通过协程整合 Retrofit 来实现网络请求,当 Activity/Fragment 销毁时,ViewModel 也自然会被销毁,如果执行网络请求的协程还没结束,则协程会被自动取消掉,避免消耗资源。
三、实现
对主要的技术点有所了解后,接下来就是具体的实现了。
1、
首先定义 BaseViewModel 基类,里边有个自定义launch
方法,参数为两个挂起函数,分别用来发起网络请求和处理异常。内部用viewModelScope
创建协程:
open class BaseViewModel : ViewModel() {
protected fun launch(request: suspend () -> Unit, fail: suspend (ApiException) -> Unit) =
viewModelScope.launch {
try {
request()
} catch (e: Throwable) {
val exception = ExceptionHandler.handle(e)
ToastUtil.show(App.getApp(), exception.errorMessage)
fail(exception)
}
}
}
2、
BaseRepository 类用来给 Retrofit 的Call
类扩展一个挂起函数await()
,来适配协程,是处理网络请求的核心方法。suspendCoroutine()
函数的作用是获取当前方法所在协程上下文,并将当前协程挂起,直到某个时机再重新恢复协程执行,当然这个时机其实是由开发者自己控制的,当网络请求失败时continuation.resumeWithException(t)
、当网络请求成功时continuation.resume(body.data)
去恢复协程的执行:
open class BaseRepository {
suspend fun <T> Call<BaseResponse<T>>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<BaseResponse<T>> {
override fun onFailure(call: Call<BaseResponse<T>>, t: Throwable) {
continuation.resumeWithException(t)
}
@Suppress("UNCHECKED_CAST")
override fun onResponse(call: Call<BaseResponse<T>>, response: Response<BaseResponse<T>>) {
val body: BaseResponse<T> = response.body() as BaseResponse<T>
if (0 != body.errorCode) {
continuation.resumeWithException(ApiException(body.errorCode, body.errorMsg))
}else{
if (body.data == null){
body.data ="" as T
}
continuation.resume(body.data)
}
}
})
}
}
}
3、
以登录功能为例,定义LoginRepository
继承BaseRepository
,负责实现登录的请求:
class LoginRepository : BaseRepository() {
suspend fun login(username: String, password: String) = withContext(Dispatchers.IO) {
val params = hashMapOf<String, String>()
params["username"] = username
params["password"] = password
RetrofitManager.create(WanAndroidApis::class.java).login(params).await()
}
}
RetrofitManager.create(WanAndroidApis::class.java).login(params)
是典型的 Retrofit 操作,返回 Call 对象,因为其中login
的api接口是这样定义的:
@POST("user/login")
fun login(@QueryMap param: Map<String, String>): Call<BaseResponse<LoginBean>>
所以继续调用了await()
方法,也就是上边的扩展函数,这样LoginRepository
就通过同步的方式实现了异步网络请求,直接得到返回结果。
4、
定义登录的LoginViewModel
类,需要调用LoginRepository
发起登录请求,并将返回结果交给 loginBean
,可以看到它是一个 LiveData 对象。
class LoginViewModel(private val repository: LoginRepository) : BaseViewModel() {
var loginBean = MutableLiveData<LoginBean>()
fun login(username: String, password: String) {
launch({
loginBean.value = repository.login(username, password)
SpUtil.setUsername(loginBean.value!!.username)
EventBus.getDefault().post(AccountEvent())
}, {
loginBean.value = null
})
}
}
5、
现在LoginViewModel
有了,但由于 ViewModel 对象不能直接创建,同时还有参数为LoginRepository
的构造函数,所以封装了一个公共方法去创建 ViewModel 对象:
fun <BVM : BaseViewModel> initViewModel(
activity: FragmentActivity,
vmClass: KClass<BVM>,
rClass: KClass<out BaseRepository>
) =
ViewModelProviders.of(activity, object : ViewModelProvider.NewInstanceFactory() {
override fun <VM : ViewModel> create(modelClass: Class<VM>): VM {
return vmClass.java.getConstructor(rClass.java).newInstance(rClass.java.newInstance()) as VM
}
}).get(vmClass.java)
接下来就是在LoginActivity
使用了:
class LoginActivity : BaseActivity() {
// 通过懒加载的形式创建 viewModel 对象
private val viewModel by lazy {
initViewModel(
this, LoginViewModel::class, LoginRepository::class
)
}
companion object {
fun start(context: BaseActivity) {
val intent = Intent(context, LoginActivity::class.java)
context.startActivity(intent)
}
}
override fun initLoad() {
}
override fun initContentView() {
setContentView(R.layout.activity_login)
}
override fun initData() {
// 监听登录结果,处理业务
viewModel.loginBean.observe(this, Observer { loginBean ->
hideLoading()
if (loginBean != null) {
finish()
}
})
}
override fun initView() {
loginBtn.setOnClickListener {
if (loginUsernameET.text.isEmpty()) {
loginUsernameTTL.error = getString(R.string.username_empty)
loginUsernameTTL.isErrorEnabled = true
return@setOnClickListener
}
if (loginPasswordET.text.isEmpty()) {
loginPasswordTTL.error = getString(R.string.password_empty)
return@setOnClickListener
}
showLoading()
// 发起登录请求
viewModel.login(loginUsernameET.text.toString(), loginPasswordET.text.toString())
}
}
}
LoginActivity
的核心功能还是比较简单的,首先创建 viewModel 对象,然后使用 viewModel 发起登录请求,再监听 viewModel 中的 loginBean 处理后续的业务。
到这里把一些核心的封装类还有使用流程都过了一遍,从项目框架结构的层面去看,再结合上边的使用,这个 MVVM 框架各部分之间的依赖关系大致如下:
为了验证这一套基础框架,真正的去使用它,所以将之前 MVP 版的 WanAndroid 重构了一遍,项目地址:https://github.com/SheHuan/WanAndroid-MVVM