简书地址:www.jianshu.com/p/77e42aebd…php
说到MVVM,你们都会想起前端的MVVM框架,相较于前端MVVM的火热,它在移动开发领域就不那么热门了。Google在2015年才推出DataBinding框架,起步较晚,并且2015年是MVP模式爆发的一年,2016年是各类热修复、插件化爆发的一年,它没遇上好时机。html
PS:DataBinding和MVVM两者并不相同。MVVM是一种架构模式,而DataBinding是Android中实现数据与UI绑定的框架,是构建MVVM架构的一个工具,做用相似于加强版的ButterKnife。前端
自16年接触DataBinding以来,苦于这方面的知识较少,可是Databinding在使用过程当中又十分便捷,因此一直以来都在不停探索怎样才能构建出合适的MVVM架构程序,在通过几回的项目重构以后,终于在近期结合Kotlin语言探索出了更适合Android的MVVM架构。android
小专栏 :使用Kotlin构建Android MVVM应用程序 Github示例:github.com/ditclear/Pa…git
咱们先来看看什么是MVVM,而后再一步一步来阐述整个的MVVM框架程序员
咱们先大体了解下Android开发的建立模式github
Model:实体模型、数据的获取、存储等等web
View:Activity、fragment、view、adapter、xml等等算法
Controller:为View层处理数据,业务等等数据库
从这个结构来看,Android自己仍是符合MVC架构的。不过因为做为纯View的xml功能太弱,以及controller能提供给开发者的做用较小,还不如在Activity页面直接进行处理,但这么作却形成了代码大爆炸。一个页面逻辑复杂的页面动辄上千行,注释没写好的话还十分很差维护,并且难以进行单元测试,因此这更像是一个Model-View的架构,不适用于打造稳定的Android项目。
Model:实体模型、数据的获取、存储等等
View:Activity、fragment、view、adapter、xml等等
Presenter:负责完成View与Model间的交互和业务逻辑,以回调返回结果。
前面说,Activity充当了View和Controller的做用, 形成了代码爆炸。而MVP架构很好的处理了这个问题。其核心理念是经过一个抽象的View接口(不是真正的View层)将Presenter与真正的View层进行解耦。Persenter持有该View接口,对该接口进行操做,而不是直接操做View层。这样就能够把视图操做和业务逻辑解耦,从而让Activity成为真正的View层。
这也是现今比较流行的架构,但是弊端也是有的。若是业务复杂了,也可能致使P层太臃肿,并且V和P层有必定耦合度,若是UI有什么地方须要更改,那么P层不仅改一个地方那么简单,还须要改View的接口及其实现,牵一发动全身,运用MVP的同行都对此怨声载道。
Model:实体模型、数据的获取、存储等等
View:Activity、fragment、view、adapter、xml等等
ViewModel:负责完成View与Model间的交互和业务逻辑,基于DataBinding改变UI
MVVM的目标和思想与MVP相似,但它没有MVP那使人厌烦的各类回调,利用DataBinding就能够更新UI和状态,达到理想的效果。
在使用MVC或MVP开发时,咱们若是要更新UI,首先须要找到这个view的引用,而后赋予值,才能进行更新。在MVVM中,这就不须要了。MVVM是经过数据驱动UI的,这些都是自动完成。数据的重要性在MVVM架构中获得提升,成为主导因素。在这种架构模式中,开发者重点关注的是怎样处理数据,保证数据的正确性。
常见的错误就是把全部代码都写在Activity或者Fragment中。任何跟UI和系统交互无关的事情都不该该放在这些类当中。尽量让它们保持简单轻量能够避免不少生命周期方面的问题。MVVM架构模式下,数据和业务逻辑都处于ViewModel中,ViewModel只关心数据和业务,不须要直接和UI打交道,而Model只须要提供ViewModel的数据源,View则关心如何显示数据和处理与用户的交互。
经过以上简述和与MVC、MVP的对比,咱们能够发现MVVM仍是颇有优点的,而若是再搭配Kotlin语言的话,能够说是如虎添翼了。
其实结构已经很清晰了,咱们只须要作M-V-VM层各层应该作的事情,作到关注点分离。
M层 的关注点是怎么提供数据给ViewModel
ViewModel层 关注点是怎么处理数据(包括使用DataBinding绑定数据,以及控制loading、empty状态)
View层的关注点是显示数据,接收用户的操做,调用ViewModel中的方法
为了打造更适合Android的MVVM架构,使用到的技术有AOP、Dagger二、RxJava、Retrofit、Room和Kotlin,并遵循统一的命名规范和调用准则,保证开发时的一致性。
如下是咱们现今的架构:
接下来我将展现一下M-V-VM三层之间如何协做,以文章详情页面为例
UI由ArtcileDetailActivity.kt及article_detail_activity.xml组成。
要驱动UI,咱们的数据模型须要持有几个元素:
咱们将建立一个ArticleDetailViewModel.kt来保存。
一个ViewModel为特定的UI组件提供数据,好比fragment 或者 activity,并负责和数据处理的业务逻辑部分通讯,好比调用其它组件加载数据或者转发用户的修改。ViewModel并不知道View的存在,也不会受configuration change影响。
如今咱们有了三个文件。
article_detail_activity.xml: 定义页面的UI
ArticleDetailViewModel.kt: 为UI准备数据的类
ArtcileDetailActivity.kt: 显示ViewModel中的数据与响应用户交互的控制器
下面开始实现(为了简单,只显示了主要部分):
<?xml version="1.0" encoding="utf-8"?>
<layout >
<data>
<import type="android.view.View"/>
<variable name="vm" type="io.ditclear.app.viewmodel.ArticleDetailViewModel"/>
</data>
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout>
<android.support.design.widget.CollapsingToolbarLayout>
<android.support.v7.widget.Toolbar app:title="@{vm.title}"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView>
<LinearLayout>
<ProgressBar android:visibility="@{vm.loading?View.VISIBLE:View.GONE}"/>
<WebView android:id="@+id/web_view" app:markdown="@{vm.content}" android:visibility="@{vm.loading?View.GONE:View.VISIBLE}"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
</layout>
复制代码
/** * 页面描述:ArticleDetailViewModel * @param repo 数据源Model(MVVM 中的M),负责提供ViewModel中须要处理的数据 * Created by ditclear on 2017/11/17. */
class ArticleDetailViewModel @Inject constructor(val repo: ArticleRepository) {
//////////////////data//////////////
lateinit var articleId:Int
val loading=ObservableBoolean(false)
val content = ObservableField<String>()
val title = ObservableField<String>()
//////////////////binding//////////////
fun loadArticle():Single<Article> =
repo.getArticleDetail(articleId)
.async()
.doOnSuccess { t: Article? ->
t?.let {
title.set(it.title)
content.set(it.content)
}
}
.doOnSubscribe { startLoad()}
.doAfterTerminate { stopLoad() }
fun startLoad()=loading.set(true)
fun stopLoad()=loading.set(false)
}
复制代码
/** * 页面描述:ArticleDetailActivity,处理和用户的交互(点击事件),以及处理 * viewModel层回调的数据,附加一些显示Loading,空状态和绑定生命周期等等的操做 * Created by ditclear on 2017/11/17. */
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
override fun getLayoutId(): Int = R.layout.article_detail_activity
@Inject
lateinit var viewModel: ArticleDetailViewModel
//init
override fun initView() {
//统一都是KEY_DATA,别本身瞎命名
val articleID: Int? = intent?.extras?.getInt(Constants.KEY_DATA)
if (articleID == null) {
toast("文章不存在", ToastType.WARNING)
finish()
}
getComponent().inject(this)
mBinding.vm = viewModel.apply {
this.articleID = articleID
}
}
//加载数据
override fun loadData() {
viewModel.loadData()
.compose(bindToLifecycle())
// .doOnSubcribe{ showLoadingDialog() }
// .doAfterTerminate{ hideLoadingDialog() }
.subscribe({},{ dispatchFailure(it) })
}
}
复制代码
他们是如何工做的呢?
在进入到ArticleDetailActivity
页面以后
进入ArticleDetailViewModel
回到ArticleDetailActivity
页面
至此,V-VM之间如何协做就清楚了。
如今咱们把View和ViewModel联系了起来,可是ViewModel该如何获取数据呢?
咱们使用Retrofit来从后端获取网络数据。
interface ArticleService{
//文章详情
@GET("article_detail.php")
fun getArticleDetail(@Query("id") id: Int): Single<Article>
}
复制代码
使用Room数据库来进行持久化
@Dao
interface ArticleDao{
@Query("SELECT * FROM Articles WHERE articleid= :id")
fun getArticleById(id:Int):Single<Article>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArticle(article :Article)
}
复制代码
而后使用ArticleRepository.kt对网络和本地操做进行一层封装
/** * 页面描述:ArticleRepository * 提供数据给ViewModel层 , 处理网络数据和本地缓存之间的关系 * Created by ditclear on 2017/11/17. */
class ArticleRepository @Inject constructor
(private val remote: ArticleService, private val local: ArticleDao) {
/* 文章详情 * 先查看本地是否有缓存,若是没有那么再去请求网络,成功后更新本地缓存 */
fun getArticle(articleId: Int): Single<Article> =
local.getArticleById(articleId).onErrorResumeNext {
if (it is EmptyResultSetException) {
remote.getArticleDetail(articleId).doOnSuccess { t -> t?.let { local.insertArticle(it) } }
} else throw it
}
}
复制代码
先查看本地是否有缓存,若是没有那么再去请求网络,成功后更新本地缓存。
封装成Repository的缘由是ViewModel不须要知道它的数据具体是从哪来的,这不是ViewModel这一层须要关心的事情。
即便你的项目没有进行数据缓存,老是从网络拉取数据,也建议封装成Repository,这意味着你的网络层是能够替换的,意义有点相似于封装一个ImageLoadUtil。
整体的流程就这么多,其实弄懂就很简单了。关键点是各层之间职责明确,以及解耦(Dagger2)和使用DataBinding时须要一个统一的规范。
而再细分,优化,也就是进行模块化、组件化的工做,深刻些的插件化、热修复等等。不过万丈高楼平地起,咱们的地基打的严实,之后的工做才会相对容易。
本文的代码均可以在github.com/ditclear/Pa…中找到
使用Presenter来继承View.OnClickListener
interface Presenter:View.OnClickListener{
override fun onClick(v: View?)
}
复制代码
而后在BaseActivity/BaseFragment里实现它
abstract class BaseActivity<VB : ViewDataBinding> : RxAppCompatActivity(),Presenter{
}
复制代码
这样当咱们要设置点击事件时,只须要
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
//...
//init
override fun initView() {
mBinding.let{
it.vm=mViewModel
it.presenter=this
}
}
}
复制代码
在xml中使用时,则统一使用presenter.onClick(view)
方法
<layout>
<data>
<variable name="presenter" type="com.ditclear.paonet.view.helper.presenter.Presenter"/>
</data>
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.FloatingActionButton android:id="@+id/stow_fab" android:onClick="@{(v)->presenter.onClick(v)}" />
</android.support.design.widget.CoordinatorLayout>
</layout>
复制代码
真正处理则放在相应的Activity/Fragment里
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
//...
@SingleClick
override fun onClick(v: View?) {
when (v?.id) {
R.id.stow_fab -> stow()
//more ..
R.id.other_action -> other()
}
}
//其它
private fun stow() {
}
//收藏
private fun stow() {
viewModel.stow().compose(bindToLifecycle())
.subscribe({ toastSuccess(it?.message?:"收藏成功") }
, { toastFailure(it) } })
}
}
复制代码
@SingleClick是一个注解,做为AspectJ的切面,来防止屡次点击,须要将view做为参数,详细可参考文章
这是这样处理点击事件的缘由之一,另外一个好处是方便绑定生命周期,和进行回调处理(好比一些须要用到activity context的dialog和toast的时候,均可以写在doOnSubscribe和doAfterTerminate操做符里),避免了ViewModel层持有context。
单元测试能保证数据和逻辑的正确性,并且语法相对简单,很容易学习。并且运行一次单元测试的时间简直毫秒杀运行一次app的时间。
我认为程序员和普通码农直接的区别之一即是是否进行单元测试。
并且因为ViewModel层是纯Kotlin/Java代码,感受就如之前使用Eclipse写简单的控制台程序。
固然单元测试的做用不只限于写测试代码,我通常都会在里面玩玩RxJava的操做符,进行一些算法的练习,验证数据的输出是否正确等等。
若是你想学习或了解单元测试,能够查看如下文章:
不少开发者放弃DataBinding缘由就在于出错了不容易排查错误。 只显示出不少XXBinding未找到。 若是有必定使用经验的就知道只看最后一条报错信息就够了。 这里介绍一种我常用来排查错误的方式: 在Android Studio 的terminal 里运行
./gradlew clean assembleDebug
或者
./gradlew compileDebugJavaWithJavac
由于DataBinding是编译生成代码的,不少错误都是xml中表达式写的有问题致使的,因此运行以上命令容易在terminal中打印出具体错误的信息。这些命令对于须要编译生成代码的框架排查错误十分有用,好比Dagger2。
更多信息请查阅 DataBinding实用指南
想要在使用DataBinding的过程当中不出错,遵照统一的规范是必定的
普通页面
ViewModel | View | XML |
---|---|---|
ArticleDetailViewModel.kt | ArticleDetailActivity.kt | article_detail_activity.xml |
列表页面 :请参考文章 告别反复、冗余的自定义Adapter
ViewModel | View | XML |
---|---|---|
ArticleListViewModel.kt | ArticleListActivity.kt | article_list_activity.xml |
Item ViewModel | Item XML | |
ArticleItemViewModel.kt | article_list_item.xml |
Model 层命名
Remote | Local | Repository |
---|---|---|
ArticleService.kt | ArticleDao.kt | ArticleRepository.kt |
结构以下图所示:
xml布局文件中的variable统一命名
ViewModel | Presenter(点击事件) | Item(列表项) |
---|---|---|
vm | presenter | item |