安卓MVVM模式

安卓MVVM模式

概念

MVVM模式是指Model-View-ViewModel。相信看过关于MVP文章的读者也会发现,无论如何抽象化,在我们的View层中是无法避免的要处理一部分逻辑的。而MVVM模式中的View是将View的状态和行为完全抽象化,把逻辑与界面的控制完全交给ViewModel处理。

安卓_MVVM模式_内容1.png

MVVM由下面三个核心组件组成:

  1. Model: 数据层,包含数据实体和对数据实体的操作
  2. View: 界面层,对应于Activity,XML,View,负责数据显示以及用户交互
  3. ViewModel: 关联层,将Model和View进行绑定,Model或者View更改时,实时刷新对方

注意点

  1. View只做和UI相关的工作,不涉及任何业务逻辑,不涉及操作数据,不处理数据。UI和数据严格的分开
  2. ViewModel只做和业务逻辑相关的工作,不涉及任何和UI相关的操作,不持有控件引用,不更新UI

------## DataBinding

什么是DataBinding

DataBinding是Google官方推出的数据绑定器,这个绑定器的作用是把数据和View绑定起来,然后数据改变的时候View会自动刷新,这个DataBinding就是我们实现MVVM模式的关键。

引入DataBinding

引入DataBinding的方式很简单,我们只需要在App的build.gradle添加如下代码即可

android{
    ...
    dataBinding {
        enabled = true
    }
}

使用DataBinding

使用DataBinding的布局文件和普通的布局文件有点不同,DataBinding布局文件的根标签是layout标签,layout里面有一个data元素和View元素,这个View元素就是我们没使用DataBinding时候的布局文件。下面看看例子代码:

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

    <data>
        <variable
            name="user"
            type="com.loaderman.modedemo.User" />
        <variable
            name="myHandlers"
            type="com.loaderman.modedemo.myHandlers" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:onClick="@{myHandlers.onClickName}"
            android:text="@{user.name}" />
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:text="更新数据"
            android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

Android MVVM模式

MVVM在不同的平台实现方式是有一定差异性的。在Google IO 2017 ,Google发布了一个官方应用架构库Architecture Components,这个架构库便是Google对Android应用架构的建议,也被称之为Android官方应用架构指南。Android Architecture Components在Google中国开发者网站中能找到。和Data Binding Library一样官方还没翻译为中文。

下图是Architecture的应用架构图。结合Android程序特点,整体上与微软的MVVM类似,但是做了更细致的模块划分。

安卓_MVVM模式_内容2.png
层次 说明
View 显而易见Activity/Fragment便是MVVM中的View,当收到ViewModel传递过来的数据时,Activity/Fragment负责将数据以你喜欢的方式显示出来。View还包括ViewDataBinding,上面中并没有体现。
ViewModel ViewModel作为Activity/Fragment与其他组件的连接器。负责转换和聚合Model中返回的数据,使这些数据易于展示,并把这些数据改变即时通知给Actvity/Fragment。<br />ViewModel是具有生命周期意识的,当Activity/Fragment销毁时ViewModel的onClear方法会被回调,你可以在这里做一些清理工作。LiveData是具有生命周期意识的一个可观察的数据持有者,ViewModel中的数据有LiveData持有,并且只有当Activity/Fragment处于活动时才会通知UI数据的改变,避免无用的刷新UI。
Model Repository及其下方就是model了。Repository负责提取和处理数据。数据来源可以是本地数据库,也可以来自网络,这些数据统一有Repository处理,对应隐藏数据来源以及获取方式。
Binder绑定器 Android中的数据绑定技术由DataBinding和LiveData共同实现。当Activity/Fragment接收到来自ViewModel中的新数据时(由LiveData自动通知数据的改变),将这些数据通过DataBinding绑定到ViewDataBinding中,UI将会自动刷新。

MVVM的实现

Model层

import java.util.List;
 
public class TestEntity {
    private int code;
    private String message;
    private List<ResultBean> result;
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public List<ResultBean> getResult() {
        return result;
    }
 
    public void setResult(List<ResultBean> result) {
        this.result = result;
    }
 
    @Override
    public String toString() {
        return "TestEntity{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", result=" + result +
                '}';
    }
 
    public static class ResultBean {
        /**
         * data : {"subTitle":null,"dataType":"TextCard","actionUrl":null,"id":0,"text":"今日社区精选","type":"header5","follow":null,"adTrack":null}
         * adIndex : -1
         * tag : null
         * id : 0
         * type : textCard
         */
 
        private DataBean data;
        private int adIndex;
        private Object tag;
        private int id;
        private String type;
 
        public DataBean getData() {
            return data;
        }
 
        public void setData(DataBean data) {
            this.data = data;
        }
 
        public int getAdIndex() {
            return adIndex;
        }
 
        public void setAdIndex(int adIndex) {
            this.adIndex = adIndex;
        }
 
        public Object getTag() {
            return tag;
        }
 
        public void setTag(Object tag) {
            this.tag = tag;
        }
 
        public int getId() {
            return id;
        }
 
        public void setId(int id) {
            this.id = id;
        }
 
        public String getType() {
            return type;
        }
 
        public void setType(String type) {
            this.type = type;
        }
 
        @Override
        public String toString() {
            return "ResultBean{" +
                    "data=" + data +
                    ", adIndex=" + adIndex +
                    ", tag=" + tag +
                    ", id=" + id +
                    ", type='" + type + '\'' +
                    '}';
        }
 
        public static class DataBean {
            /**
             * subTitle : null
             * dataType : TextCard
             * actionUrl : null
             * id : 0
             * text : 今日社区精选
             * type : header5
             * follow : null
             * adTrack : null
             */
 
            private Object subTitle;
            private String dataType;
            private Object actionUrl;
            private int id;
            private String text;
            private String type;
            private Object follow;
            private Object adTrack;
 
            public Object getSubTitle() {
                return subTitle;
            }
 
            public void setSubTitle(Object subTitle) {
                this.subTitle = subTitle;
            }
 
            public String getDataType() {
                return dataType;
            }
 
            public void setDataType(String dataType) {
                this.dataType = dataType;
            }
 
            public Object getActionUrl() {
                return actionUrl;
            }
 
            public void setActionUrl(Object actionUrl) {
                this.actionUrl = actionUrl;
            }
 
            public int getId() {
                return id;
            }
 
            public void setId(int id) {
                this.id = id;
            }
 
            public String getText() {
                return text;
            }
 
            public void setText(String text) {
                this.text = text;
            }
 
            public String getType() {
                return type;
            }
 
            public void setType(String type) {
                this.type = type;
            }
 
            public Object getFollow() {
                return follow;
            }
 
            public void setFollow(Object follow) {
                this.follow = follow;
            }
 
            public Object getAdTrack() {
                return adTrack;
            }
 
            public void setAdTrack(Object adTrack) {
                this.adTrack = adTrack;
            }
 
            @Override
            public String toString() {
                return "DataBean{" +
                        "subTitle=" + subTitle +
                        ", dataType='" + dataType + '\'' +
                        ", actionUrl=" + actionUrl +
                        ", id=" + id +
                        ", text='" + text + '\'' +
                        ", type='" + type + '\'' +
                        ", follow=" + follow +
                        ", adTrack=" + adTrack +
                        '}';
            }
        }
    }
}

Respository用户从服务器获取数据,这里使用的网络框架是retrofit+rxjava+okhttp:

public class TestRespository {
    public void getData(final MutableLiveData<TestEntity> liveData) {
        HttpServiceDataProvder.getInstence().loadTestData(new HttpCallBack<TestEntity>() {
            @Override
            public void onSuccess(TestEntity testEntity) {
                liveData.setValue(testEntity);
            }

            @Override
            public void onError(Throwable e) {
            }
        });
    }
}

ViewModel层

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

import com.loaderman.frameappdemo.mvvm.model.TestEntity;
import com.loaderman.frameappdemo.mvvm.repository.TestRespository;

public class TestViewModel extends ViewModel {
    private final TestRespository testRespository;
    private MutableLiveData<TestEntity> liveData;

    public MutableLiveData<TestEntity> getTestEntityLiveData() {
        if (liveData == null) {
            liveData = new MutableLiveData<>();
        }
        return liveData;
    }

    public TestViewModel() {
        testRespository = new TestRespository();
    }

    public void getDataFromNet() {
        testRespository.getData(liveData);
    }
}

View层

布局文件

<?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="test"
            type="com.loaderman.frameappdemo.mvvm.model.TestEntity" />
    </data>

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".mvvm.view.MVVMActivity">
            <TextView
                android:id="@+id/tv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="@{test.result.toString()}"></TextView>
        </LinearLayout>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>

Activity文件

import com.loaderman.frameappdemo.BR;
import com.loaderman.frameappdemo.R;
import com.loaderman.frameappdemo.databinding.ActivityMvvmBinding;
import com.loaderman.frameappdemo.mvvm.model.TestEntity;
import com.loaderman.frameappdemo.mvvm.viewmodel.TestViewModel;

public class MVVMActivity extends AppCompatActivity {
    private ActivityMvvmBinding viewDataBinding;
    private TestViewModel testViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvvm);
        viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_mvvm);
        testViewModel = new ViewModelProvider(this).get(TestViewModel.class);
        MutableLiveData<TestEntity> testViewModelData = testViewModel.getTestEntityLiveData();
        initData();
        testViewModelData.observe(this, new Observer<TestEntity>() {
            @Override
            public void onChanged(TestEntity testEntity) {
                viewDataBinding.refresh.setRefreshing(false);
                viewDataBinding.setVariable(BR.test, testEntity);
            }
        });

        viewDataBinding.refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                initData();
            }
        });
    }

    private void initData() {
        viewDataBinding.refresh.setRefreshing(true);
        testViewModel.getDataFromNet();
    }
}

实际效果

安卓_MVVM模式_内容3.gif

在listview 等列表的adapter中使用数据绑定,可以使用如下方式:

LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
DataBindingUtil.inflate(inflater,R.layout.activity_main, viewgroup,false);

MVVM的特点

MVVM的优势

  1. 双向绑定技术,当Model变化时,View-Model会自动更新,View也会自动变化。很好做到数据的一致性,不用担心,在模块的这一块数据是这个值,在另一块就是另一个值了。所以 MVVM模式有些时候又被称作:model-view-binder模式。
  2. View的功能进一步的强化,具有控制的部分功能,若想无限增强它的功能,甚至控制器的全部功几乎都可以迁移到各个View上(不过这样不可取,那样View干了不属于它职责范围的事情)。View可以像控制器一样具有自己的View-Model
  3. 由于控制器的功能大都移动到View上处理,大大的对控制器进行了瘦身。不用再为看到庞大的控制器逻辑而发愁了。
  4. 可以对View或ViewController的数据处理部分抽象出来一个函数处理model。这样它们专职页面布局和页面跳转,它们必然更一步的简化。

MVVM的劣势

  1. 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
  2. 一个大的模块中,model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存,就造成了花费更多的内存。
  3. 数据双向绑定不利于代码重用。客户端开发最常用的重用是View,但是数据双向绑定技术,让你在一个View都绑定了一个model,不同模块的model都不同。那就不能简单重用View了。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容