内容简介:作者:SheHuan链接:https://www.jianshu.com/p/1c18d1d3ae49
code小生 一个专注大前端领域的技术平台 公众号回复 Android
加入安卓技术群
作者:SheHuan
链接:https://www.jianshu.com/p/1c18d1d3ae49
声明:本文已获 SheHuan
授权发表,转发等请联系原作者授权
一、问题
相信大家对 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
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Django 1.0 Template Development
Scott Newman / Packt / 2008 / 24.99
Django is a high-level Python web application framework designed to support the rapid development of dynamic websites, web applications, and web services. Getting the most out of its template system a......一起来看看 《Django 1.0 Template Development》 这本书的介绍吧!
RGB转16进制工具
RGB HEX 互转工具
SHA 加密
SHA 加密工具