《Android构建MVVM》系列(一) 之 MVVM架构快速入门

前言

  本文属于《Android构建MVVM》系列开篇,共六个篇章,详见目录树。java

  该系列文章旨在为Android的开发者入门MVVM架构,掌握其基本开发模式。android

  辅以讲解Android Architecture Components,使得更好的实现MVVM架构。git

目录树

  • 《Android构建MVVM》系列(一) 之 MVVM架构快速入门
    • 前言
    • 分层思想
    • 什么是MVC/MVP?
    • MVVM是什么,与MVC/MVP有何区别?
    • Android Architecture Components(架构组件)
    • 一个MVVM的Demo
    • 结语
  • 《Android构建MVVM》系列(二) 之 架构组件LiveData
  • 《Android构建MVVM》系列(三) 之 架构组件ViewModel
  • 《Android构建MVVM》系列(四) 之 架构组件Room
  • 《Android构建MVVM》系列(五) 之 构建更好的Demo
  • 《Android构建MVVM》系列(六) 之 总结与展望

正文

1、分层思想

  层是一种思想,同时也是一种架构模式。它的特色是专职,即某一层的职责是相同的、肯定的;好比咱们平时所谓的Dao、Controller层…他们都有明确的职责。github

  分层思想具体表现为,经过抽象某一类的逻辑构成一个水平功能面,进而对上层提供API;多个层面相互依赖、配合提供总体解决方案。层与层之间的依赖关系是自上而下的,即上层依赖下层,下层不能依赖上层,最底层的组件没有依赖。编程

  初学者每每搞不明白为何明明能够直接编码业务逻辑,还要去作所谓的分层架构呢?json

    其实对仅仅实现需求来讲,用不用分层架构没有关系的,不分层照样能够实现;那么为何咱们还要徒增烦恼呢?有句话说的好:“存在即合理”,也就是说既然咱们的前辈研究出了所谓的分层架构,而且沿用至今;那么就必定有它的优势,必定是解决了某一领域的痛点而诞生的api

    试想如下场景:当你的项目随着需求的增长和不断调整,不可避免的就要去改动已有的代码,若是项目规模不大还好,若是是个老古董项目,可能某个类里面上万行的代码,没有注释,没有采用分层架构,相信我,你会哭的缓存

  分层架构虽然说不能完彻底全的解决项目程序复杂度高的问题,可是经过分层,将大的问题抽象分解成了小的功能面,局部化在每一层中,这样就有效的下降了单个问题的规模和复杂度;另外层与层之间也能够经过简单的调整,插入新的层面,用以知足不断变化的需求,同一层面来讲也可近乎0成本的水平扩展;而且易于后期的Debug、测试。微信

2、什么是MVC/MVP?

  来讲说MVC吧,其实MVX模式都是分层思想的一种具体实现,上文提到的分层思想其实是一种抽象层面的分层,着重表如今抽象和解耦。网络

  MVC其实是一种分层思想的践行者和改进者,在GUI编程中,MVC已经有几十年的历史了。

    顾名思义M(Model)即数据模型层,Model层颇有意思,对于服务端编程来讲咱们把MVC中的M极有多是包括了业务处理(Service)和实体类的,对于客户端编程来讲MVC中的M可能就仅仅是数据模型,固然以上的说法只是于我我的而言的体会,不表明广义立场。

  V(View)即视图层/表现层,主要负责数据的展现和用户的交互,C(Controller)即控制层,主要负责一些数据传递、请求转发、业务处理的委派。

  以上是标准意义上的MVC,对于Android来讲:

Model:数据模型(实体类、持久化、IO)
View:布局文件
Controller:对应于Activity、Fragment,包含一些业务逻辑的处理

  这里咱们会发现,Android的MVC事实上V层的职责一部分被C层承担了,好比一些Activity/Fragment中不可避免的一些交互逻辑等,这样就会致使C层既包含UI交互,又有网络请求、业务处理等;致使C层过于臃肿,不利于项目后期的维护和扩展。

  因此,MVP就应运而生了,MVP其实是由MVC进化而来,它比较好的解决了MVC时代遗留的问题,MVP中的各层含义:

Model:数据模型(实体类、持久化、IO)
View:Activity/Fragment和布局文件
Presenter:负责完成View和Model之间的交互和业务逻辑

  其核心思想是:设计一个抽象的V层接口,并由具体的View实现该接口,P层内部维护一个该接口的实例引用,通常在构造函数中传递进来赋值(即View层初始化P层实例时),彼时P层便可经过调用该接口来完成对View层的操做,V层也因持有P层实例,能够进行业务逻辑处理委派。

3、MVVM是什么?与MVC/MVP有何区别?

  MVVM是对MVP/MVC的一种改进,既解决了MVC时代的职责不明的问题,也很好的解决了MVP模式中须要编写过多繁琐的接口,以及V层和P层互相依赖所产生的一些隐式问题。

  在MVVM中,各层含义以下:

Model:数据模型(实体类、持久化、IO)
View:Activity/Fragment和布局文件
ViewModel:业务逻辑的处理、数据的转换、链接M层和V层的桥梁

  看上去彷佛和MVP中各层的职责是相似的,并无显著的不一样和改进;那么咱们为什么要使用MVVM架构呢?

  引入美团技术团队的一段解释

    1. 双向绑定、数据驱动
      在常规的开发模式中,数据变化须要更新UI的时候,须要先获取UI控件的引用,而后再更新UI。获取用户的输入和操做也须要经过UI控件的引用。在MVVM中,这些都是经过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层,数据成为主导因素。这样MVVM层在业务逻辑处理中只要关心数据,不须要直接和UI打交道,在业务处理过程当中简单方便不少。

    2. 高度解耦
      MVVM模式中,数据是独立于UI的。
      数据和业务逻辑处于一个独立的ViewModel中,ViewModel只须要关注数据和业务逻辑,不须要和UI或者控件打交道。UI想怎么处理数据都由UI本身决定,ViewModel不涉及任何和UI相关的事,也不持有UI控件的引用。即使是控件改变了(好比:TextView换成EditText),ViewModel也几乎不须要更改任何代码。它很是完美的解耦了View层和ViewModel,解决了上面咱们所说的MVP的痛点。

    3. 可复用、易测试、方便协同开发
      一个ViewModel能够复用到多个View中。一样的一份数据,能够提供给不一样的UI去作展现。对于版本迭代中频繁的UI改动,更新或新增一套View便可。若是想在UI上作A/B Testing,那MVVM是你不二选择。
      MVVM的分工是很是明显的,因为View和ViewModel之间是松散耦合的:一个是处理业务和数据、一个是专门的UI处理。因此,彻底由两我的分工来作,一个作UI(XML和Activity)一个写ViewModel,效率更高
      ViewModel层作的事是数据处理和业务逻辑,View层中关注的是UI,二者彻底没有依赖。无论是UI的单元测试仍是业务逻辑的单元测试,都是低耦合的。在MVVM中数据是直接绑定到UI控件上(部分数据是能够直接反映出UI上的内容),那么咱们就能够直接经过修改绑定的数据源来间接作一些Android UI上的测试。

4、Android Architecture Components(架构组件)

  现MVVM的方式和工具备不少,既可使用Google在2015年推出的DataBinding库,亦或是其余。也能够选择Google IO 2017 推出的Android Architecture Components即架构组件,亦或是其余方式。

  本文采用的解决方案:使用Architecture Components架构MVVM。

  上图是官方给出的架构模型,包含如下组件:

  • 生命周期管理库 - Lifecycle

    • Lifecycle组件,为下面两个组件提供了生命周期感知的基础

    • LiveData组件,可观测的、可感知生命周期的数据

    • ViewModel组件,不依赖于View、提供UI数据、桥接持久层、业务逻辑

  • 数据持久化库 - Room,Sqlite的ORM

   事实上,Architecture Components实现了一个比较理想化的依赖方式,自上而下,单向依赖;VM层并不持有View层的任何引用,但倒是生命周期感知的,在新版的AS中View也不用去实现某些接口或继承特定的类,AppCompatActivity已经帮你整合了这一切。

  另外,关于Repository的解释,它并非架构组件的成员,可是Google推荐引入Repository层,来做为咱们惟一的数据来源接口,咱们从图中也很好理解,他的职责就是使VM层对数据来源是无感知的,包装了数据来源,提供统一的API,供上层透明化的调用。

  更多的关于Android Architecture Components的教程,欢迎关注咱们后续的架构组件篇章。

5、一个MVVM的Demo

  面咱们经过设计App《每日美文》的Demo,并使用Architecture Components架构MVVM的方式去完成。

  这个Demo使用Kotlin开发,没接触过Kotlin的童鞋也没必要担忧,本文没有用到Kotlin的一些高级特性,只须要Google花个半小时时间学习基本的Kotlin语法即可无障碍阅读

  项目地址:https://github.com/xykjlcx/OneArticleDemo

1. 首先咱们建立工程

 

项目建立完成后的目录结构

架构组件的相关依赖

// livedata viewmodel
    def lifecycle_version = "1.1.1"
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"
    implementation "android.arch.lifecycle:viewmodel:$lifecycle_version"
    implementation "android.arch.lifecycle:livedata:$lifecycle_version"
    implementation "android.arch.lifecycle:runtime:$lifecycle_version"
    annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version"

  PS:视图(布局xml)就不带你们看了,很简单,有兴趣的童鞋能够直接到Github上看源码

2. 工具类:OceanUtil

  网络请求封装的有些随意,轻喷😂

/**
 * Created by ocean on 2018/8/11
 * Author :  ocean
 * Email  :  348686686@qq.com
 */

object OceanUtil{

    object Holder{
        val OK_HTTP_CLIENT = OkHttpClient()
        val GSON = Gson()
    }

    private const val TAG:String = "ocean"

    /**
     * 网络请求工具方法demo
     * @param url api接口地址
     * @param handler
     */
    fun httpRequest(url: String,params: HashMap<String,Any>,handler: Handler){
        var jsonObject = JSONObject(params)
        var requestBody = RequestBody.create(AppConstant.MEDIA_TYPE_JSON,jsonObject.toString())
        val okHttpClient = Holder.OK_HTTP_CLIENT
        val request = Request.Builder()
                .url(url)
                .post(requestBody)
                .build()
        okHttpClient.newCall(request).enqueue(object: Callback{
            override fun onFailure(call: Call?, e: IOException?) {
                logE("请求失败")
            }

            override fun onResponse(call: Call?, response: Response?) {
                logE("请求成功")
                val result:String = response!!.body().string().toString()
                val message = Message()
                message.what = 200
                message.obj = result
                handler.sendMessage(message)
            }

        })
    }

    /**
     * 日志打印
     * @param any
     */
    fun logE(any: Any) {
        if (AppConstant.isDegug)
            Log.e(TAG,"-> -> -> 日志打印【 $any 】")
    }

    /**
     * 数据转换
     */
    fun convertData(result:String): ArticleModel {
        return Holder.GSON.fromJson(
                result,
                object : TypeToken<ArticleModel>(){}.type
        )
    }

}

OK,下面咱们将自下而上的构建MVVM

3. 第一步:建立实体Model(Java混编)

  Ps:使用了GsonFormat暂时还不支持Kotlin的数据类(data class),因此这里采用混编的方式。

  这里咱们使用GsonFormat插件直接生成Java实体类,也就是咱们的每日美文的Model。

  都是一些属性和get/set方法,咱们用到的字段也就三四个,你们浏览一遍便可。

/**
 * Created by ocean on 2018/8/11
 * Author :  ocean
 * Email  :  348686686@qq.com
 */
public class ArticleModel{

    /**
     * ResultCode : 1
     * ErrCode : OK
     * Body : {"id":4861,"vol":"VOL.2135","img_url":"http://image.wufazhuce.com/FhUGpJBjkcod8DHH7OSieT-8ODKz","img_author":"Ethan Yang","img_kind":"摄影","date":"2018-08-11 06:00:00","url":"http://m.wufazhuce.com/one/2165","word":"本身被伤害的时候,有的人生气,有的人伤心。生气的人是憎恨的,将本身束之高阁而去攻击对方,歇斯底里地喊叫起来。懂得悲伤的人,必定懂得爱,只是静静地如时间停滞般,独自哀伤。人们总说爱恨参半,其实这是不可能存在的,既爱之,何恨之。","word_from":"《高岭之花》","word_id":2165}
     */

    private int ResultCode;
    private String ErrCode;
    private BodyBean Body;

    public int getResultCode() {
        return ResultCode;
    }

    public void setResultCode(int ResultCode) {
        this.ResultCode = ResultCode;
    }

    public String getErrCode() {
        return ErrCode;
    }

    public void setErrCode(String ErrCode) {
        this.ErrCode = ErrCode;
    }

    public BodyBean getBody() {
        return Body;
    }

    public void setBody(BodyBean Body) {
        this.Body = Body;
    }

    @Override
    public String toString() {
        return "ArticleModel{" +
                "ResultCode=" + ResultCode +
                ", ErrCode='" + ErrCode + '\'' +
                ", Body=" + Body +
                '}';
    }

    public static class BodyBean {
        /**
         * id : 4861
         * vol : VOL.2135
         * img_url : http://image.wufazhuce.com/FhUGpJBjkcod8DHH7OSieT-8ODKz
         * img_author : Ethan Yang
         * img_kind : 摄影
         * date : 2018-08-11 06:00:00
         * url : http://m.wufazhuce.com/one/2165
         * word : 本身被伤害的时候,有的人生气,有的人伤心。生气的人是憎恨的,将本身束之高阁而去攻击对方,歇斯底里地喊叫起来。懂得悲伤的人,必定懂得爱,只是静静地如时间停滞般,独自哀伤。人们总说爱恨参半,其实这是不可能存在的,既爱之,何恨之。
         * word_from : 《高岭之花》
         * word_id : 2165
         */

        private int id;
        private String vol;
        private String img_url;
        private String img_author;
        private String img_kind;
        private String date;
        private String url;
        private String word;
        private String word_from;
        private int word_id;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getVol() {
            return vol;
        }

        public void setVol(String vol) {
            this.vol = vol;
        }

        public String getImg_url() {
            return img_url;
        }

        public void setImg_url(String img_url) {
            this.img_url = img_url;
        }

        public String getImg_author() {
            return img_author;
        }

        public void setImg_author(String img_author) {
            this.img_author = img_author;
        }

        public String getImg_kind() {
            return img_kind;
        }

        public void setImg_kind(String img_kind) {
            this.img_kind = img_kind;
        }

        public String getDate() {
            return date;
        }

        public void setDate(String date) {
            this.date = date;
        }

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getWord() {
            return word;
        }

        public void setWord(String word) {
            this.word = word;
        }

        public String getWord_from() {
            return word_from;
        }

        public void setWord_from(String word_from) {
            this.word_from = word_from;
        }

        public int getWord_id() {
            return word_id;
        }

        public void setWord_id(int word_id) {
            this.word_id = word_id;
        }

        @Override
        public String toString() {
            return "BodyBean{" +
                    "id=" + id +
                    ", vol='" + vol + '\'' +
                    ", img_url='" + img_url + '\'' +
                    ", img_author='" + img_author + '\'' +
                    ", img_kind='" + img_kind + '\'' +
                    ", date='" + date + '\'' +
                    ", url='" + url + '\'' +
                    ", word='" + word + '\'' +
                    ", word_from='" + word_from + '\'' +
                    ", word_id=" + word_id +
                    '}';
        }
    }
}

4. 第二步:引入Repository层

  前面咱们说过,Repository层是为了屏蔽底层数据来源,透明的被上层所调用

class ArticRepository(application: Application){

    private var liveData = MutableLiveData<ArticleModel>()

    init {
       getHttpData()
    }

    fun getLiveDta():MutableLiveData<ArticleModel>{
        return liveData
    }

    fun getHttpData(){
        val params : HashMap<String,Any> = HashMap()
        params["TransCode"] = "030111"
        params["OpenId"] = "123456789"
        OceanUtil.httpRequest(
                AppConstant.URL,
                params,
                object : Handler(){
                    override fun handleMessage(msg: Message?) {
                        super.handleMessage(msg)
                        val result = msg?.obj as String
                        OceanUtil.logE("打印请求结果:$result")
                        liveData.value = OceanUtil.convertData(result)
                    }
                }
        )
    }

}

  能够看到,内部获取数据实际上使用的就是Okhttp的工具方法,不过对于调用者来讲,上层并不关心数据是从Sqlite读出来的,仍是网络请求响应的,亦或是其余数据来源。这样在Repository层咱们能够很轻松的完成缓存、数据转化等操做,而不影响上层。
  后面的文章,咱们会使用Room对网络数据进行持久化缓存,在无网络环境下,保证用户使用软件的完整性,给用户更好的体验。

5. 第三步:建立你的ViewModel

  咱们能够选择继承ViewModel/AndroidViewModel类来编写咱们项目的ViewModel实现

/**
 * Created by ocean on 2018/8/12
 * Author :  ocean
 * Email  :  348686686@qq.com
 */
class ArticleViewModel(application: Application) : AndroidViewModel(application) {

    private var repository : ArticRepository? = null
    private var data:MutableLiveData<ArticleModel>? = null

    init {
        repository = ArticRepository(application)
        data = repository?.getLiveDta()
    }

    fun getData(): MutableLiveData<ArticleModel>? {
        return data
    }

    fun requestData(){
        repository?.getHttpData()
    }

}

  VM层看起来很简洁,是的。得益于MutableLiveData(LiveData的子类),咱们没必要要作不少复杂的工做;就像这样,咱们仅仅只是声明了一个MutableLiveData的引用、获取实例、调用Repository层获得数据这样微小的工做。

6. 第四步:在View层使用

  在View层调用VM很是简单,Architecture Components的开发者已经帮咱们处理了这一切。

  另外,在Fragment中使用ViewModel咱们一般在ViewModelProviders.of()方法中传入getActivity();事实上因为传入的Context相同(同一个activity),咱们获得的VM也是相同的,因此ViewModel还能够处理Fragment之间的通讯。

class MainActivity : AppCompatActivity() {

    private var vm : ArticleViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 获取vm对象
        vm = ViewModelProviders.of(this).get(ArticleViewModel::class.java)
        initData()
    }

    fun initData(){
        btn_get.setOnClickListener(View.OnClickListener {
            vm?.requestData()
        })
        vm?.getData()?.observe(this, Observer {
            it?.let { it1 -> updateView(it1) }
        })
    }

    fun updateView(model: ArticleModel){
        tv_title.text = model.body.word_from
        tv_author.text = "—— " +  model.body.img_author
        tv_digest.text = model.body.word
        Glide
                .with(this)
                .load(model.body.img_url)
                .dontAnimate()
                .centerCrop()
                .into(img_left)
        Toast.makeText(this,"更新成功",Toast.LENGTH_SHORT).show()
    }

}

  就像这样,咱们只须要经过ViewModelProviders.of().get方法获得VM的引用,接下来咱们只须要获取LiveData对象的引用,对其添加.observe()方法,以后组件便会自动观察LiveData数据的变化,当数据发生变化时,系统会调用Observer接口的onChanged()方法,因此咱们只须要将更新UI的逻辑写在onChanged()方法体内便可。

7. 告一段落

  至此,咱们基本已经完成了这个Demo,应该说仍是蛮Easy的,后期咱们会迭代初一个更好的Demo。

  但愿你们持续关注个人微信公众号😀

Ps:附上接口地址

// url接口地址:https://api.hibai.cn/api/index/index
// 入参:"TransCode"="030111","OpenId":"123456789"
// 这是接口返回json数据
{
    "ResultCode": 1,
    "ErrCode": "OK",
    "Body": {
        "id": 4860,
        "vol": "VOL.2136",
        "img_url": "http://image.wufazhuce.com/FpXhraPX6RVOiEVBkxL2mJzd1Lb5",
        "img_author": "狐狸狐狸鱼",
        "img_kind": "插画",
        "date": "2018-08-12 06:00:00",
        "url": "http://m.wufazhuce.com/one/2166",
        "word": "仍是想在夏天与你相恋,不只是夜晚温热的风,清爽的白色短袖,仍是冰镇西瓜或者幻想中的漫长暑假,多是曾经以为夏天就属于慵懒,因此才会以为要搭配一个你,在懒洋洋里带着些许的紧张。是你啊,又见面了。",
        "word_from": "咸贵人",
        "word_id": 2166
    }
}

6、结语

  知不觉已经写了这么多了,这是做者第一次写这么长的技术文章。
  在发稿前,Review文章;总绝对没有符合“单一职责”原则
  本身也在想,技术类文章在讲重心以前,作一些前置知识点的解释,是否必要。好比本文:分层思想->MVC/MVP->MVVM,相比较开门见山的讲解MVVM是更好理解仍是以为更加臃肿呢?
  因此,笔者也但愿你们若是读了这篇文章,能够在留言区评论本身的感觉,我将进一步改善文章框架,尽可能让你们能够高效的学习。
  最后,笔者技术能力和文笔能力有限,有什么写的不妥的地方,也请你们予以斧正和谅解。