使用Kotlin构建更适合Android的MVVM应用程序

112 阅读7分钟

现在我们有了三个文件。

article_detail_activity.xml: 定义页面的UI

ArticleDetailViewModel.kt: 为UI准备数据的类

ArtcileDetailActivity.kt: 显示ViewModel中的数据与响应用户交互的控制器

下面开始实现(为了简单,只显示了主要部分):

<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>

</android.support.design.widget.CoordinatorLayout>

/**

  • 页面描述: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() val title = ObservableField()

//////////////////binding////////////// fun loadArticle():Single

= 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() {

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页面之后

  1. init()方法->先进行数据的初始化,将viewModel和xml文件进行绑定
  2. loadData()方法->调用viewModel的方法

进入ArticleDetailViewModel

  1. 调用Model层获取详情方法获取数据源
  2. 根据需要使用RxJava操作符对数据进行转换,通过DataBinding更新UI
  3. 返回可观测的Single对象给View

回到ArticleDetailActivity页面

  1. 绑定生命周期,避免内存泄漏
  2. 对返回的可观测对象进行订阅
  3. 处理成功和失败的情况

至此,V-VM之间如何协作就清楚了。

M—VM

现在我们把View和ViewModel联系了起来,但是ViewModel该如何获取数据呢?

我们使用Retrofit来从后端获取网络数据。

interface ArticleService{ //文章详情 @GET("article_detail.php") fun getArticleDetail(@Query("id") id: Int): Single

}

使用Room数据库来进行持久化

@Dao interface ArticleDao{

@Query("SELECT * FROM Articles WHERE articleid= :id") fun getArticleById(id:Int):Single

@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 = 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…中找到

一些建议

建议一:在Activity或Fragment里处理点击事件

使用Presenter来继承View.OnClickListener

interface Presenter:View.OnClickListener{ override fun onClick(v: View?) }

然后在BaseActivity/BaseFragment里实现它

abstract class BaseActivity : RxAppCompatActivity(),Presenter{

}

这样当我们要设置点击事件时,只需要

class ArticleDetailActivity : BaseActivity() {

//... //init override fun initView() {

mBinding.let{ it.vm=mViewModel it.presenter=this } } }

在xml中使用时,则统一使用presenter.onClick(view)方法

<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>

真正处理则放在相应的Activity/Fragment里

class ArticleDetailActivity : BaseActivity() {

//... @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作为参数,详细可参考文章

DataBinding结合AspectJ防止多次点击

这是这样处理点击事件的原因之一,另一个好处是方便绑定生命周期,和进行回调处理(比如一些需要用到activity context的dialog和toast的时候,都可以写在doOnSubscribe和doAfterTerminate操作符里),避免了ViewModel层持有context。

建议二:多写单元测试

单元测试能保证数据和逻辑的正确性,而且语法相对简单,很容易学习。而且运行一次单元测试的时间简直毫秒杀运行一次app的时间。

我认为程序员和普通码农直接的区别之一便是是否进行单元测试。

而且由于ViewModel层是纯Kotlin/Java代码,感觉就如以前使用Eclipse写简单的控制台程序。

当然单元测试的作用不仅限于写测试代码,我一般都会在里面玩玩RxJava的操作符,进行一些算法的练习,验证数据的输出是否正确等等。

如果你想学习或了解单元测试,可以查看以下文章:

关于安卓单元测试,你需要知道的一切(by 小创)

使用Kotlin和RxJava测试MVP架构的完整示例

关于DataBinding

很多开发者放弃DataBinding原因就在于出错了不容易排查错误。 只显示出很多XXBinding未找到。 如果有一定使用经验的就知道只看最后一条报错信息就够了。 这里介绍一种我经常使用来排查错误的方式: 在Android Studio 的terminal 里运行

./gradlew clean assembleDebug

或者

./gradlew compileDebugJavaWithJavac

因为DataBinding是编译生成代码的,很多错误都是xml中表达式写的有问题导致的,所以运行以上命令容易在terminal中打印出具体错误的信息。这些命令对于需要编译生成代码的框架排查错误十分有用,比如Dagger2。

最后

文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

由于文章篇幅问题 查看详细文章以及获取学习笔记链接:GitHub

  • Android进阶学习全套手册 关于实战,我想每一个做开发的都有话要说,对于小白而言,缺乏实战经验是通病,那么除了在实际工作过程当中,我们如何去更了解实战方面的内容呢?实际上,我们很有必要去看一些实战相关的电子书。目前,我手头上整理到的电子书还算比较全面,HTTP、自定义view、c++、MVP、Android源码设计模式、Android开发艺术探索、Java并发编程的艺术、Android基于Glide的二次封装、Android内存优化——常见内存泄露及优化方案、.Java编程思想 (第4版)等高级技术都囊括其中。

  • Android高级架构师进阶知识体系图 关于视频这块,我也是自己搜集了一些,都按照Android学习路线做了一个分类。按照Android学习路线一共有八个模块,其中视频都有对应,就是为了帮助大家系统的学习。接下来看一下导图和对应系统视频吧!!!

  • Android对标阿里P7学习视频

  • BATJ大厂Android高频面试题 这个题库内容是比较多的,除了一些流行的热门技术面试题,如Kotlin,数据库,Java虚拟机面试题,数组,Framework ,混合跨平台开发,等