mvvm - mvvm框架的入门使用

  历时4个月多,mvvm的第一个版本总算开发完成,心中的石头也算是落下了。想起去年的国庆节,7天假期没有迈出家门一步,抱着一本《kotlin 实战》书死磕,每每磕到深夜,在七天里面算是对kotlin这门语言入门了。学习了kotlin之后,为了学习Google爸爸最新的组件--jetpack,又跑到官方网站去死磕,前前后后不知道看了几遍文档,生怕漏了一点知识点。当然官方文档上介绍的知识都比较简单,为了深入的学习jetpack组件,又跑到国外的知名博客网站--medium学习大牛们分享的相关技术博客。
  jetpack学习差不多两个多月吧,自认为自己学的挺好的。于是,为了巩固学习成果,动手写了这个mvvm框架--jade-mvvm。本文将简单介绍mvvm的整个结构,以及入门使用。

1. mvvm的分层

  整个mvvm的分层参考于官方的介绍--应用架构指南,整个结构也符合如下图所示:


  相比于官方推荐的结构,我在此基础扩展了两个点:

  1. View的职责分离。如果按照官方的推荐,View相关的操作必须在View层,这就意味着很多业务将耦合在View里面,可能会导致ActivityFragment,为了减轻ActivityFragment的负担,同时为了将相关职责分离和整合,我在从MVP架构里面吸取了相关经验,将View拆分为了多个Presenter,每个Presenter只负责自己相关的业务即可。需要注意的是Presenter仍然属于View层。
  2. 将repository整合成一个。按照官方的推荐,repository应该明确区分,比如说,从内存中加载数据的repository和从网络上加载数据的repository应该是不一样的。而我认为,这两个repository在上层的逻辑都是一样的,唯一的区别就是数据的来源,所以我在repository层做了统一,统一从request层去获取数据,由request层去决定数据的来源。

  所以mvvm框架总结下来,便是如下的结构:


2. 基本使用

  在mvvm框架里面,每个页面都有Activity+Fragment组成的,Activity本身不会承载很多的业务,所有业务都应该收敛在Fragment里面。

(1).Activity和Fragment的创建

  每个Activity必须直接或者间接继承于BaseActivity,每个Fragment必须直接或者间接继承于BaseFragment。通常情况下,Activity只负责加载一个Fragment即可,当然也可以处理页面上的事情,比如说,window的feature和theme等。
  下面展示一个简单的例子(可以从jade-mvvm获得完整的代码):

class MessageDetailActivity : BaseActivity() {

    private val mMessageId by ExtraDelegate(MESSAGE_ID, 0)

    override fun buildCurrentFragment() = MessageDetailFragment.newInstance()

    override fun buildFragmentArguments() = Bundle().apply {
        putInt(MESSAGE_ID, mMessageId)
    }

    companion object {
        const val MESSAGE_ID = "MESSAGE_ID"
    }
}

  上面便是一个Activity的代码,它的工作主要加载一个Fragment,我们来看看对应的Fragment的代码:

class MessageDetailFragment : BaseFragment<MessageDetailViewModel>() {

    private val mMessageId by ExtraDelegate(MessageDetailActivity.MESSAGE_ID, 0)

    override fun getLayoutId() = R.layout.fragment_message_detail

    override fun onCreateViewModel() =
        ViewModelProvider(this, MessageDetailViewModel.Factory(mMessageId))[MessageDetailViewModel::class.java]

    override fun onCreatePresenter() = Presenter().apply {
        addPresenter(MessageDetailRefreshPresenter())
        addPresenter(MessageDetailInitViewPresenter())
    }

    companion object {
        fun newInstance() = MessageDetailFragment()
    }
}

  Fragment做的事情相对来说就比较多了,首先是创建一个自己的ViewModel,其次将一些相关业务抽到不同的Presenter里面。我这里将只是简单的定义了两个Presenter,其中:MessageDetailRefreshPresenter用来负责刷新相关操作,MessageDetailInitViewPresenter负责将加载回来的数据展示到View层。

需要特别注意的是:Presenter本身属于View层的一部分,所以请勿做一些非View层的事情,比如说请求数据。如果想要请求数据,应当通知ViewModel,让ViewModel去请求,Presenter此时只需要做一件事,监听对应的LiveData,等待数据更新在更新View层即可。

  例如,MessageDetailRefreshPresenter的代码将展示如何正确的请求数据:

class MessageDetailRefreshPresenter : Presenter() {

    @Inject(Constant.VIEW_MODEL)
    lateinit var mMessageDetailViewModel: MessageDetailViewModel

    private val mRefreshLayout by lazy {
        getRootView().findViewById<SwipeRefreshLayout>(R.id.refresh_layout)
    }

    override fun onBind() {
        mMessageDetailViewModel.mLoadStatusLiveData.observe(getCurrentFragment()!!, Observer<LoadStatus> {
            mRefreshLayout.isRefreshing = it == LoadStatus.LOADING_REFRESH
        })
        mRefreshLayout.setOnRefreshListener {
            mMessageDetailViewModel.refresh()
        }
        mMessageDetailViewModel.trRefresh()
    }
}

(2).如何正确创建一个ViewModel

  前面已经说了, 每个Fragment需要一个ViewModel。所以在创建好一个Fragment之后,就需要定义一个与之对应的ViewModel。在mvvm框架里面,已经定义两种ViewModel:BaseViewModelBaseRecyclerViewModel。其中BaseViewModel用于普通Fragment;BaseRecyclerViewModel用于带有RecyclerView的Fragment,推荐这类Fragment继承于RecyclerViewFragment,其中BaseRecyclerViewModel集成了jetpack组件里面的paging库,所以使用BaseRecyclerViewModel,网络加载这块就不需要我们过多的关心。
  为了介绍的简单,本文将以BaseViewModel为例,简单介绍它的基本使用。如果想要了解BaseRecyclerViewModel,可以看PositionViewModelPositionViewModel.kt
  要想创建一个ViewModel,首先要继承于BaseViewModel

class MessageDetailViewModel(id: Int) : BaseViewModel<Message>(MessageDetailRepository(id)) {

    class MessageDetailRepository(private val id: Int) : BaseRepository<Message>() {
        override fun getRequest() = MessageDetailRequest(id)
    }

    class Factory(private val id: Int) : ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel?> create(modelClass: Class<T>) = MessageDetailViewModel(id) as T
    }
}

  创建一个ViewModel之后,我们还需要给ViewModel创建一个Repository对象,这个Repository对象的作用主要是加载数据,就如前文所说,Repository里面主要从Request层获取数据,这个数据可以从本地获取,也可以从网络上获取,这个不是ViewModel和Repository关心的。
  在mvvm框架里面定义三个Repository接口,分别是:

  1. ItemKeyedRepository:主要是跟BaseItemKeyedDataSource搭配使用。
  2. ListPositionRepository:主要是跟BasePositionDataSource搭配使用。
  3. Repository:通用的接口,只要不符合上面两种情况,均可以使用。

  在ViewModel中,除了定义Repository,每个实现者都可以按需定义很多的LiveData,用来达到的业务要求。关于LiveData的基本使用以及需要注意的事项,可以参考Google爸爸的博客:

  1. LiveData 概览
  2. ViewModels and LiveData: Patterns + AntiPatterns
  3. LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData
  4. LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

3. 结语

  jade-mvvm是我花了4个多月琢磨出来的,该框架完全遵从Google的建议。当然这个过程中,肯定有所不足的地方,欢迎大家积极提意见。接下来的日子,我将深入的分析jetpack库中的各个组件源码,像学习RecyclerView源码一样,系统性的学习和分析底层源码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268