用程式碼手把手教你使用MVVM

Android機動車發表於2017-11-12

原始碼請點選:github.com/shuaijia/Js…

您還可以關注我的微信公眾號——安卓乾貨營,與我交流和獲取更多精彩內容。

概述

說到Android MVVM,就會聯想到DataBinding框架。然而兩者的概念是不一樣的,不能混為一談。MVVM是一種架構模式,而DataBinding是一個實現資料和UI繫結的框架,是構建MVVM模式的一個工具。

網上關於MVVM框架的搭建和使用的文章很少,大多提到MVVM框架,就是在介紹DataBinding的使用。對於MVVM中各模組之間如何劃分,如何定義,又是如何配合實現高度解耦的文章更是少之又少。大家看完後還是一頭霧水,只是對MVVM有個大概的瞭解,並不很清楚如何上手。

接下來,我們先認識什麼是MVVM,然後再一步一步來設計整個MVVM框架。

MVC、MVP簡介

MVC、MVP和MVVM都是在安卓開發中經常使用的模式,我們在認識MVVM之前先回顧一下MVC和MVP。

MVC

  • View:xml佈局
  • Model:資料層,負責資料互動、儲存和實體類定義
  • Controller:業務處理層

Android開發本身還是比較符合MVC架構的,但是Android中純粹作為View的XML檢視功能太弱,我們大量處理View的邏輯只能寫在Activity中,這樣Activity就充當了View和Controller兩個角色,直接導致Activity中的程式碼臃腫、混亂,導致閱讀困難、重用困難和維護困難。相信大多數Android開發者都遇到過一個Acitivty數以千行的程式碼情況吧!所以,更貼切的說法是,這個MVC結構最終其實只是一個Model-View(Activity:View&Controller)的結構。

MVP

  • View:xml檔案及對應的Activity或Fragment,負責介面展示和互動
  • Model:資料層,負責資料互動、儲存和實體類定義
  • Presenter:負責View層和Model層之間的邏輯處理

前面我們說,Activity充當了View和Controller兩個角色,MVP就能很好地解決這個問題,其核心理念是通過一個抽象的View介面(不是真正的View層)將Presenter與真正的View層進行解耦。Persenter持有該View介面,對該介面進行操作,而不是直接操作View層。這樣就可以把檢視操作和業務邏輯解耦,從而讓Activity成為真正的View層。

不足的是,MVP模式中定義了大量的介面,使得程式碼結構變大和複雜;MVP是UI和事件驅動,需要手動呼叫大量的方法來進行實現,缺乏自動性。

所以我們迎來了MVVM框架,當然得首先感謝google爸爸提供得DataBinding,真的是很強大!

MVVM簡介

這裡寫圖片描述

在MVVM模式中,將程式結構分為三層——View-ViewModel-Model,接下來我們一起來認識它們:

View:

View層負責和UI相關的工作,我們只在XML、Activity和Fragment寫View層的程式碼,View層不進行業務處理,也就是我們在Activity不寫業務邏輯和業務資料相關的程式碼。

更新UI通過資料繫結實現,儘量在ViewModel裡面做,Activity要做的事就是初始化一些控制元件(如RecyclerView設定LayoutManager或者控制元件的顯隱),View層可以通過資料來驅動更改UI,UI事件通過Command來繫結。

簡而言之:View層不做任何業務邏輯、不涉及運算元據,UI和資料嚴格的分開。 **UI更新和事件相應全部使用資料繫結,也就是DataBinding來實現。**這就是MVVM和MVP、MVC很明顯的不同之處。

ViewModel

ViewModel層做的事情剛好和View層相反,ViewModel只負責業務邏輯,不做任何和UI相關的事情。

同時DataBinding框架已經支援雙向繫結,讓我們可以通過雙向繫結獲取View層反饋給ViewModel層的資料,並對這些資料上進行操作。

事件的處理,我們也希望能把這些事件處理繫結到控制元件上,並把這些事件的處理統一化,為此我們通過使用BindingAdapter對一些常用的事件做封裝,把一個個事件封裝成一個個Command,對於每個事件我們用一個ReplyCommand去處理就行了,ReplyCommand會把你可能需要的資料帶給你,這使得我們在ViewModel層處理事件的時候只需要關心處理資料就行了,具體見MVVM Light Toolkit 使用指南的Command部分。

Model

Model層不僅包括實體類的定義,還需要對資料進行處理和讀寫。例如:使用Retrofit或okHttp進行網路請求,或著如資料庫操作等等。

優點:

  • 資料驅動
  • 低耦合
  • 主執行緒更新UI
  • 可複用性
  • 方便單元測試

我們再來看下這張圖:

這裡寫圖片描述

簡述下資料流走向:

View中使用DataBinding的Command來繫結事件和響應事件,觸發網路請求;ViewModel進行分析處理,呼叫Model的資料請求方法;Model將收到的請求引數等資訊封裝,呼叫網路請求庫;網路庫(Retrofit等)與伺服器進行互動;

伺服器將json資料返回Retrofit等網路庫,再返回到Model層中,ViewModel在回撥中收到返回的實體類物件;

因為xml與實體類物件實現了雙向繫結,實體類更新,使得UI更新!

ok!接下來我們就用活生生的例子來實現MVVM吧

A、實體類定義

/**
 * Description:
 * Created by jia on 2017/11/3.
 * 人之所以能,是相信能
 */
public class NewslistBean extends BaseObservable {
 
    private String ctime;
    private String title;
    private String description;
    private String picUrl;
    private String url;

    public NewslistBean(String ctime, String title, String description, String picUrl, String url) {
        this.ctime = ctime;
        this.title = title;
        this.description = description;
        this.picUrl = picUrl;
        this.url = url;
    }

    public String getCtime() {
        return ctime;
    }

    public void setCtime(String ctime) {
        this.ctime = ctime;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getPicUrl() {
        return picUrl;
    }

    public void setPicUrl(String picUrl) {
        this.picUrl = picUrl;
    }

    public String getUrl() {
        return url;
    }

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

    @BindingAdapter("bind:imageUrl")
    public static void loadImage(ImageView imageView, String picUrl) {
        Glide.with(imageView.getContext())
                .load(picUrl)
                .into(imageView);
    }

    public void onItemClick(View pView) {
        Toast.makeText(pView.getContext(), title, Toast.LENGTH_SHORT).show();
    }

}
複製程式碼

這和平時寫的實體類是不是沒啥區別!

是的,所有的屬性我們依舊如原來原來一樣定義和設定get、set方法。但是,有一點不同的是實體類繼承了BaseObservable,稍後我們再說。

B、Model類

/**
 * Description: 新聞網路請求model
 * Created by jia on 2017/11/3.
 * 人之所以能,是相信能
 */
public class NewsModel {

    public void getNews(final OnCallBack onCallBack) {
        HttpMethod.getInstance().getNews(new Subscriber<News>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                onCallBack.onFaile(e.toString());
            }

            @Override
            public void onNext(News news) {
                onCallBack.onSuccess(news);
            }
        });
    }


    public interface OnCallBack {
        void onSuccess(News news);

        void onFaile(String errorInfo);
    }
}
複製程式碼

這裡呢,我使用的是自己封裝過的Retrofit+RxJava的網路請求庫,上面的Model用來進行新聞實體類News的網路請求;

也定義了一個CallBack介面:此回撥可以讓接下的ViewModel獲得Model請求回來的實體類。

每個專案的網路請求庫和方法都會不同,符合自己的就是最好的!(●ˇ∀ˇ●)

C、View

xml中

先看示例:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="news"
            type="com.jia.jsmvvm.home.viewmodel.NewslistBean" />

    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".home.view.MainActivity">

        <android.support.v7.widget.CardView
            android:id="@+id/cv_tuijian"
            android:layout_width="match_parent"
            android:layout_height="130dp"
            android:layout_margin="15dp"
            android:background="#ffffff"
            android:elevation="5dp"
            android:onClick="@{news.onItemClick}"
            app:cardElevation="5dp">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="5dp">

                <ImageView
                    android:id="@+id/iv_tuijian"
                    android:layout_width="120dp"
                    android:layout_height="match_parent"
                    android:layout_margin="15dp"
                    android:scaleType="fitXY"
                    app:imageUrl="@{news.picUrl}" />

                <TextView
                    android:id="@+id/tv_tuijian_title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignTop="@id/iv_tuijian"
                    android:layout_toRightOf="@id/iv_tuijian"
                    android:ellipsize="end"
                    android:maxLines="2"
                    android:text="@{news.title}"
                    android:textColor="#111111"
                    android:textSize="18sp" />

                <TextView
                    android:id="@+id/tv_tuijian_time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentBottom="true"
                    android:layout_alignParentRight="true"
                    android:ellipsize="end"
                    android:lines="3"
                    android:singleLine="true"
                    android:text="@{news.ctime}"
                    android:textColor="#777777"
                    android:textSize="14sp" />
            </RelativeLayout>
        </android.support.v7.widget.CardView>


    </RelativeLayout>
</layout>
複製程式碼

大家可定已經發現了:佈局的編寫和往常比還是又較大變化的。

熟悉DataBinding的朋友可以直接跳過這趴。由於本人對DataBinding也不是特別熟練,所以也只能和大家分享自己瞭解的一點使用方法。DataBinding擁有非常強大的功能,想深入瞭解的可以網上搜尋,當然,本人不久也會把自己瞭解的DataBinding的知識整理成一篇部落格,敬請期待!

  1. 我們使用 layout 作為佈局檔案的跟節點
  2. layout中包含data節點和普通的佈局
  3. data節點中建立variable
  4. variable中有兩個“屬性”:name和type
  5. type宣告實體類,格式為 包名.類名
  6. name為type中的實體類定義“名字”,供以下佈局中使用
  7. 定義了data屬性後,就相當於xml佈局已和實體類繫結
  8. 在控制元件中引用實體類屬性的格式為: @{實體類.屬性名}
  9. 在控制元件中引用實體類方法的格式為: @{實體類.方法名}
  10. 涉及到圖片載入:在實體類中使用@BindingAdapter註解圖偏載入方法,在佈局中引用url即可

因為本篇文章重點在於講述MVVM框架的使用,所以DataBinding只進行粗略簡介,如有錯誤,還望大家及時提出,我們一起進步!

Activity中

在Activity中設定佈局,我們不再使用Activity的setContentView方法,取而代之的是:DataBindingUtil.setContentView

ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
複製程式碼

所返回的變數型別怎麼來的呢?

將使用了DataBinding的佈局名字,去掉所有下劃線,將所有單詞首字母大寫,直接進行拼接,最後加上 Binding即可!

View層中這樣就可以了!哈哈!

D、ViewModel

ViewModel層大家比較不熟悉,他和MVC的Controller、MVP的Presenter到底有什麼區別呢?

ViewModel類應該怎麼寫呢?

先看下程式碼:

/**
 * Description: 新聞ViewModel類
 * Created by jia on 2017/11/3.
 * 人之所以能,是相信能
 */
public class NewsViewModel {

    public Activity activity;

    public ActivityMainBinding activityMainBinding;

    public NewslistBean news;

    public NewsModel model;

    private int num=1;

    public NewsViewModel(Activity activity, final ActivityMainBinding activityMainBinding) {
        this.activity = activity;
        this.activityMainBinding = activityMainBinding;

        model=new NewsModel();

        model.getNews(new NewsModel.OnCallBack() {
            @Override
            public void onSuccess(News news) {
                activityMainBinding.setNews(news.getNewslist().get(0));
            }

            @Override
            public void onFaile(String errorInfo) {
                news=new NewslistBean("error","error","error","error","error");
                activityMainBinding.setNews(news);
            }
        });
    }

}
複製程式碼

看看裡邊有些啥:

  • Context或Activity物件(這個應該好理解把)
  • 在Activity中建立的Binding物件
  • 實體類物件
  • Model層物件
  • ChildViewModel(例如Activity中巢狀多個Fragment的情況)

將實體類物件通過setXXX方法,設定給Binding物件。

當事件觸發時,Model進行網路請求,在回撥中更新實體類,便可對應的更新UI介面。

總結

例項中只是一個簡單的功能的展示,大家在熟悉了MVVM後可再深度封裝。

本文主要講解了一些本人再開發過程中總結的Android MVVM構建思想,更多是理論上各個模組如何分工、程式碼如何設計。雖然在現實生產中用Android MVVM模式開發還比較少,但是隨著DataBinding 1.0的釋出,相信在Android MVVM 這一領域會更多的人來嘗試。

由於時間有限,能力有限,文中不免有錯誤或不足的地方,還請大家提出,我們一同進步!

原始碼請點選:github.com/shuaijia/Js…

您還可以關注我的微信公眾號——Android機動車,獲取更多精彩內容!

用程式碼手把手教你使用MVVM

相關文章