Android MVP && MVVM深度解析

前言

相信很多同学对MVP和mvvm都玩的很6了,但本文还是想从2个框架的特性、优缺点来深层次解析一下,帮助大家更好的理解框架。本文有深度,也有故事,下面开车。

MVP

MVP.png

这里引用官方的一张图来简单介绍MVP模式,可以看出Model层是真正处理数据的,Presenter是联系M和V的中介,P持有M和V的引用,P和V是双向引用。

看一个最基础的MVP

##Model
class LoginModel : BaseModel() {
    fun login(userName: String, pwd: String): Int {
        //...省略网络请求
        return 1
    }
}

##Presenter
class LoginPresenter(var iLoginView: ILoginView) :
    BasePresenter<LoginModel, ILoginView>(iLoginView) {
    init {
        mModel = LoginModel()
    }

    fun login(userName: String, pwd: String) {
        var loginResult = mModel?.login(userName, pwd)
        iLoginView.loginResult(loginResult == 1)
    }
}

##View
class MVPActivity : BaseMVPActivity<LoginModel,ILoginView>(),ILoginView {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mvp)
        
        loginBtn.setOnClickListener { 
            (mPresenter as LoginPresenter).login("u1","123")
        }

    }

    override fun initPresenter() {
        mPresenter=LoginPresenter(this)
    }

    override fun loginResult(isSuccess: Boolean) {
        TODO("Not yet implemented")
    }

    override fun showLoading() {
        TODO("Not yet implemented")
    }

    override fun dismissLoading() {
        TODO("Not yet implemented")
    }

    override fun loadFailure() {
        TODO("Not yet implemented")
    }
}

这个案例非常简单,就是一个登录校验操作,可以看出Activity把业务逻辑都委托给Presenter操作,Activity只需要做发出请求/根据请求结果展示UI即可,从分层思想上来说还是很清晰的。下面先小结一下:

1.核心要点

把数据和业务逻辑从视图层(View)剥离出来,V和P通过接口回调来通信。

2、优点

  • 这种分层思想在一定程度实现了解耦,符合类的单一职责设计原则;
  • m、v、p 3层都可以复用,p也可以做单独的单元测试;

从上面的代码可以看出至少2点,第1,Activity/Fragment职责变轻了,代码量也就少了;第2,像登录操作在大多数项目是有多处会用到的,这样的话,就可以把它单独抽出来,下次只需要创建LoginPresenter对象就可以调用登录功能了,总结一下就是那些可以复用的逻辑都可以抽离成单独的MVP。在没有UI界面的时候,你甚至可以单独调试Presenter。

那MVP模式有没有什么缺点或者不足呢?答案是有的。第1,MVP充斥着大量的接口,你的model、view、presenter都需要写接口,这个任务量是很繁重的,而且类的数量会很快膨胀;第2,V和P还有一定耦合,如果V层某个UI元素更改,那相关的接口也需要更改,非常麻烦。第3,内存泄漏问题,比如说,Activity还在做网络请求,用户等不及退出了,由于P持有View的引用,Activity无法及时回收,就发生了内存泄漏;小结一下:

3、缺点

  • 接口太多,类膨胀问题;
  • 在业务逻辑比较复杂的时候,接口粒度大小不好控制。粒度太小,就会存在大量接口的情况;粒度太大,解耦效果不好。
  • P和V需要通过接口交互,还是存在一定耦合,算不上真正的解耦;如果接口有所变化的时候,需要改动的地方太多;
  • 内存泄漏和空指针问题。由于P和V是互相引用,如果页面销毁时P还有正在进行的任务,那Activity无法回收,就发生了内存泄漏。

下面针对这些缺点,提出一些解决思路

接口太多,类膨胀问题
网上一些方案是引入契约类,就是接口套接口的形式,把MVP相关的接口都整合在一起。Google官方的demo确实引入了Contract。

public interface Contract {

    interface Model extends BaseModel {

    }

    interface View extends BaseView<Presenter> {

    }


    interface Presenter extends BasePresenter {

    }
}

但是这样其实是不好的。你可能会觉得这样让逻辑更紧密,代码更好读。但这样做,反而违背了mvp解耦的本质,让代码更加复杂了。此外,在复用的情况,比如一个Presenter有多个Model,一个Activity有多个Presenter,这种情况下用契约类就比较复杂,不好处理。

接口粒度
之前说过接口粒度大小难以控制。先说一下粒度太小,如果多个Activity有相同的回调方法,我把他做成单独的接口,比如我的页面有网络请求中、请求失败、登录、上传图片等可以复用的接口方法,那我就需要实现多个接口,这样会造成接口太多。如果粒度太大,每个接口会有很多方法,就会出现很多重复的内容,也就变得耦合了。
再举个例子,我有一个业务,包含增删改查4个方法,但是A页面只需改查、B页面只需要增删,按接口设计来说,我应该写在一起吧,这样就会造成2个页面都会实现2个空方法,这是粒度太大;如果我把它拆成2个接口吧,接口数量会增加,这是粒度太小。关于接口粒度,目前不好解决。

P和V耦合
此外P和V之间通过接口回调来交互,还是存在耦合的,没有完全实现视图和业务的解耦。如果因为需求变更导致接口有所改动,需要改动的地方太多。总的来说,关于接口问题目前来说是没有完美的解决办法的。

内存泄漏问题
刚才说过内存泄漏是因为P持有V的引用,导致gc来的时候发现m->p->v这条GC引用链存在,就不会回收Activity,于是Activity内存泄漏了。解决思路:在onDestroy()断开引用关系,并取消网络任务。

override fun onDestroy() {
        super.onDestroy()
       //  防止内存泄漏
        mPresenter?.onDestroy()
        mPresenter = null
    }

class ePresenter{
      fun onDestroy() {
          //取消网络请求
          cancalNetTask()
          mView = null
    }
}

也可以通过弱引用来解决

class TestPresenter<M : BaseModel, V : IBaseView>(view: V) {

    var iView: WeakReference<V>? = null

    init {
        iView = WeakReference(view)
    }

    fun onDestroy() {
        iView?.clear()
        iView = null
    }
}

MVVM

说一段历史

现在网上仍然充斥着大量不规范的MVVM的文章,百度首页很多都是,其中也包括我在17年写的一篇,所谓不规范是指这些MVVM仅仅是在MVP基础上引入DataBinding,就被当作MVVM模式了。 我来解释一下这个情况,mvvm和MVP的区别有2点,第1,vm和v是单向引用;第2,基于观察者模式把数据从vm传给View,v和vm不再需要接口回调来联系。

mvvm其实分为2个阶段,在2017之前,是基于databinding的,在2017之后是基于AAC架构的,也就是livedata、viewmodel相关。由于在16,17年Jetpack相关的Viewmodel、LiveData还没有推广开,在2017之前要把数据从vm传给v是比较麻烦,不用接口回调的话,用观察者模式来做是比较方便的,但是那时候livedata还没有出来,就只能用databinding的观察者模式或自己手写观察者,由于这样做比较麻烦,很多人甚至直接沿用接口回调去更新UI数据。正是由于当时的技术和认知不足以及很多误导博客的广泛传播,导致了一部分人以为MVP+databinding就是mvvm了。

故事说完了,下面来了解一下MVVM的特性和实现吧。

mvvm.png

图片来自https://juejin.im/post/5c2f43796fb9a04a04412a18

1、核心要点
数据和UI完全解耦、数据驱动、不存在内存泄漏问题、代码更简洁。可以说解决了MVVM大部分弊端。

2、优点

从设计上解决了内存问题
在MVP中存在内存泄漏问题,需要手动管理,很是麻烦;而MVVM从系统设计上解决了这个问题,开发者再也不需要担心内存问题了。V和VM是单向引用,VM不持有任何View相关的对象,这样就解决了内存泄漏。由于ViewModel和LiveData内部都是通过lifecycle关联生命周期,会在页面正常销毁的时候(onDestory),解除观察者,销毁自身。

数据驱动
数据变化自动更新UI,用户输入和操作需要数据自动更新,可以通过LiveData和DataBinding来完成,二者都是基于观察者模式。

数据和UI完全解耦
数据和业务逻辑都在的ViewModel中,ViewModel只需要关注数据和业务逻辑,完全不需要管UI操作和变化。

更新UI
在子线程操作完数据之后,可以直接更新ViewModel的数据即可,不需要考虑线程切换,因为ViewModel中的LiveData已经帮我们做了这个事情。

mvvm基础

##Model
class NewsModel {
    /**
     * 模拟加载网络数据
     */
    fun loadDataFromNet(): String {
        //...省略网络操作
        return "this data from net"
    }
}

##ViewModel
class NewsViewModel : ViewModel() {
    private val mModel by lazy {
        NewsModel()
    }

    val liveData = MutableLiveData<String>()

    fun loadData() {
        var result = mModel.loadDataFromNet()
        //更新数据
        liveData.value = result
    }
}


##Activity
class MvvmActivity : AppCompatActivity() {

    private val newsVm: NewsViewModel by lazy {
          ViewModelProvider(this).get(NewsViewModel::class.java)
      }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_mvvm)

        initLiveData()
        newsVm.loadData()
    }

    private fun initLiveData() {
        newsVm.liveData.observe(this, object : Observer<String> {
            override fun onChanged(t: String?) {
                //更新UI
                textView.text = t
            }
        })
    }
}

这是最基础的基于AAC方案的MVVM,没有过度封装。当然你也可以结合databinding库来使用。

我还想说明一点,一个项目中你可以同时使用mvc、MVP、mvvm,这取决于你的业务,记住一点,框架始终是为业务服务的。欢迎下方留言,说出你的观点。

感谢以下作者

Android架构设计---MVP模式第(二)篇,如何减少类爆炸
https://blog.csdn.net/qq137722697/article/details/78275882
https://blog.csdn.net/u011033906/article/details/89448350
https://tech.meituan.com/2016/11/11/android-mvvm.html
Android MVVM架构分析
从最简单的Android MVP讲起

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