Android中MVVM是什么?

Posted 周文凯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android中MVVM是什么?相关的知识,希望对你有一定的参考价值。

转载请标明出处:
https://blog.csdn.net/xuehuayous/article/details/82777022
本文出自:【Kevin.zhou的博客】

前言:去年对项目的架构进行了调整,迁移到了MVVM架构,还好之前的代码写的还算清晰,在调整的过程中也没有遇到太多的问题。改造的过程中也查找了大量资料,不管是架构相关的还是针对MVVM的代码或文章,发现在android中对于MVVM大家还没有达成共识。或许每一种方式在固定的业务场景中都是有益的,并没有谁对谁错。之前在技术群里也听到有很多人想了解MVVM,正好我也总结下希望对想了解的朋友能起到帮助作用;如果有不同的意见,也许是我功力尚浅,希望能够留下评论,我们共同探讨,相互学习。

关于什么是MVC/MVP/MVVM以及他们之间的区别,这里就不赘述了,如果想了解网上的资料很多,而且很容易陷入他们的对比,最终搞的云里雾里。

一句话了解MVVM

  1. View 只处理用户的即时交互;
  2. ViewModel 只处理业务逻辑;
  3. Model 只处理数据存储与获取。

View

View层,不再是我们之前理解的是一个TextView、LinearLayout等View控件,只要是可以和用户进行交互的都可以归属到View层,比如:Activity、Fragment、Dialog、PopupWindow、XML布局、布局Adapter、系统及自定义View控件等等,只要是用户看到的、摸到的都归View层管。也就是说禁止在这里做业务逻辑、数据操作等和View无直接相关的事情。

Model

model层,与我们之前把定义的实体bean对象称为model不同的是,这里的model被赋予了数据管理的职责。数据的管理包括数据存储与数据获取,这里存储的位置不仅限于本地(SharedPreferences、SQLite),而且也会是网络上的任何存储方式。

ViewModel

ViewModel层,说是只处理业务逻辑,更准确的说法是Model层和View层的粘合剂,从Model中获取数据整合之后提供给View层进行显示,响应View层的事件调用Model层进行响应的落地。

它们的关系

通过以上大致了解了View-ViewModel-Model是什么、主要干什么,通过这三个层次的定义把视图、业务、数据进行了切割,这样职责与分层清楚了,那他们之间怎么传递数据呢?

View层包含两大类,Activity、Fragment、Dialog、PopupWindow等代码类和XML布局类,代码类与布局类交互的方式是DataBinding,通过DataBinding的双向绑定技术可以轻松实现View数据的双向通知。

Model层数据的获取与保存绝大部分应该是在服务端的,网络访问是数据交互的重头戏,这部分必须在异步中完成,异步处理方面大家都弃Handler而去投奔了Rx的怀抱,所有在Model层的网络部分与ViewModel层交互通过Rx的观察者模式Observable。

ViewModel层从Model层获取数据后,需求将数据通知到View层并显示出来,采用Android架构组件中的LiveData。

Demo

通过以上说明,大家应该是对MVVM架构有了大致的了解,并且具体怎么操作是云里雾里。下面就以一个示例的方式进行说明。思考再三我决定用Java而不是Kotlin来进行编写,尽管Kotlin用起来很舒服,但是照顾到可能一部分朋友不太了解。

需求一

需求内容

编写字符串,保存到本地,再读取出来修改下并保存。

撸码

XML布局

布局很简单,上面一个EditText,下面是两个Button。

也许有朋友对这种布局的编写有些疑惑,请移步上一篇博客《认识Android中的双向绑定》 ,对Android中的databinding进行初识。
其中android:text="@=view.content"是做数据双向绑定用的;
android:onClick="@view::onSaveClick"即可调用view中的onSaveClick(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">

    <data class="com.kevin.mvvmdemo.MainActivityBinding">

        <variable
            name="view"
            type="com.kevin.mvvmdemo.MainActivity" />
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <EditText
            android:id="@+id/et_content"
            android:layout_width="0dp"
            android:layout_height="125dp"
            android:background="#EEEEEE"
            android:hint="请输入内容"
            android:inputType="textMultiLine"
            android:padding="8dp"
            android:text="@=view.content"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btn_save"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="保存"
            android:onClick="@view::onSaveClick"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/btn_load"
            app:layout_constraintTop_toBottomOf="@+id/et_content"
            app:layout_constraintVertical_chainStyle="spread" />

        <Button
            android:id="@+id/btn_load"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="加载"
            android:onClick="@view::onLoadClick"
            app:layout_constraintLeft_toRightOf="@+id/btn_save"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/et_content"
            app:layout_constraintVertical_chainStyle="spread" />

    </android.support.constraint.ConstraintLayout>
</layout>

编写MainActivity

创建MainActivity,Activity中为设置databinding及两个回调方法,onSaveClick(view)onLoadClick(view)这两个方法是在XML布局中调用的。

public class MainActivity extends AppCompatActivity 

    private MainActivityBinding mBinding;
    public MutableLiveData<String> content = new MutableLiveData<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        mBinding = MainActivityBinding.inflate(getLayoutInflater());
        mBinding.setLifecycleOwner(this);
        setContentView(mBinding.getRoot());
        mBinding.setView(this);
    

    public void onSaveClick(View view) 
        Toast.makeText(this, "点击了 保存 按钮", Toast.LENGTH_SHORT).show();
    

    public void onLoadClick(View view) 
        Toast.makeText(this, "点击了 加载 按钮", Toast.LENGTH_SHORT).show();
    

目前看到的效果是这样的,Activity中响应XML布局的的点击。

注意
这里需要注意的是,在《认识Android中的双向绑定》中可观察对象使用的是:

public ObservableField<String> content = new ObservableField<>();

这里使用的是:

public MutableLiveData<String> content = new MutableLiveData<>();

其中ObservableField是databinding中提供的,MutableLiveData是android arch 中提供的,二者都可以,Google推荐是MutableLiveData,Android架构会管理它的生命周期,在使用MutableLiveData,需要添加如下:

mBinding.setLifecycleOwner(this);

编写MainRepository

创建MainRepository,Repository主要做的就是数据的获取与保存,这里简单的保存在SharedPreferences,提供了保存和读取的两个方法。

public class MainRepository 

    private Application mApplication;

    public MainRepository(Application application) 
        this.mApplication = application;
    

    public void saveData(String content) 
        SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE);
        sp.edit().putString("content", content).commit();
    

    public String loadData() 
        SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE);
        return sp.getString("content", "");
    

编写MainViewModel

创建MainViewModel,继承自Android架构组件中的ViewModel或者AndroidViewModel,其中AndroidViewModel运行传递Application对象。
作为业务逻辑的处理者以及Model层与View层的粘结剂,这里没有需要处理的物业逻辑,仅仅是对数据层封装了一下,提供给View层使用。

public class MainViewModel extends AndroidViewModel 

    private MainRepository mRepository;

    public MainViewModel(@NonNull Application application) 
        super(application);
        mRepository = new MainRepository(application);
    

    public void setData(String content) 
        mRepository.saveData(content);
    

    public String getData() 
        return mRepository.loadData();
    

修改MainActiviy

添加依赖库

实例化ViewModel需要用到Android架构组件中的另外一个库,在build.gradle中添加依赖:

dependencies 
 	// ... ...
    implementation 'android.arch.lifecycle:extensions:latest.release'

实例化ViewModel

在onCreate()方法中初始化ViewModel:

mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
调用ViewModel方法

之后就可以在点击的监听中进行响应的操作啦,代码如下:

    public void onSaveClick(View view) 
        mViewModel.setData(content.getValue());
    

    public void onLoadClick(View view) 
        String data = mViewModel.getData();
        content.setValue(data);
    
完整代码
public class MainActivity extends AppCompatActivity 

    public MutableLiveData<String> content = new MutableLiveData<>();

    private MainActivityBinding mBinding;
    private MainViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        mBinding = MainActivityBinding.inflate(getLayoutInflater());
        mBinding.setLifecycleOwner(this);
        setContentView(mBinding.getRoot());
        mBinding.setView(this);
        mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
    

    public void onSaveClick(View view) 
        mViewModel.setData(content.getValue());
    

    public void onLoadClick(View view) 
        String data = mViewModel.getData();
        content.setValue(data);
    


效果

通过以上,就完成了一个最简单的MVVM,效果如下:

大家可能觉得这么简单的东西,我在Activity中简单的几行就能搞定,你却来这么一堆调用,不是很麻烦。那么我们做一下修改呢?

需求二

需求内容

由于输入的字数不能够无线长,那我们限制最长为100个字,太少了也不行,最少10个字,如果不合法就提示。

撸码

修改布局

增加两个文案

<TextView
    android:id="@+id/tv_max_count"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="4dp"
    android:layout_marginRight="8dp"
    android:text="/100"
    android:textColor="@color/gray"
    android:textSize="12sp"
    app:layout_constraintBottom_toBottomOf="@+id/et_content"
    app:layout_constraintRight_toRightOf="@+id/et_content" />

<TextView
    android:id="@+id/tv_count"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="4dp"
    android:textSize="12sp"
    app:layout_constraintBottom_toBottomOf="@+id/et_content"
    app:layout_constraintRight_toLeftOf="@+id/tv_max_count"
    tools:text="0" />

大家先想一下,如果用原生的怎么搞?给EditText控件设置内容更改监听,当变化后将获取内容长度设置给TextView。不复杂,就是一堆模板代码。
但是如果使用databinding,也许你不信,两行代码就搞定。

<TextView
    android:id="@+id/tv_count"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="4dp"
    android:text="@String.valueOf(view.content.length())"
    android:textColor="@(view.content.length() > 9 &amp;&amp; view.content.length() &lt; 101) ? @color/gray : @color/red, default=@color/red"
    android:textSize="12sp"
    app:layout_constraintBottom_toBottomOf="@+id/et_content"
    app:layout_constraintRight_toLeftOf="@+id/tv_max_count"
    tools:text="0" />

一行是动态绑定text,一行是动态设置颜色。
其中设置文案的比较好理解,就是获取可观察对象(输入内容)的长度,设置给TextView显示。

android:text="@String.valueOf(view.content.length())"

下面设置颜色的,可观察对象(输入内容)的长度大于9 并且 小于 101 则显示灰色,其他情况显示红色。

android:textColor="@(view.content.length() > 9 &amp;&amp; view.content.length() &lt; 101) ? @color/gray : @color/red, default=@color/red"

通过以上演示大家已经看到了View层databinding的好处。

如果输入不合法的时候不允许提交怎么做呢?想必大家都想到了在XML中处理。

<Button
    android:id="@+id/btn_save"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:enabled="@view.content.length() > 9 &amp;&amp; view.content.length() &lt; 101, default=false"
    android:onClick="@view::onSaveClick"
    android:text="保存"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toLeftOf="@+id/btn_load"
    app:layout_constraintTop_toBottomOf="@+id/et_content"
    app:layout_constraintVertical_chainStyle="spread" />

也是一行代码就可以搞定:

android:enabled="@view.content.length() > 9 &amp;&amp; view.content.length() &lt; 101, default=false"

需求三

需求内容

如果我想保存到网络呢?

撸码

本想着在我的服务器上搭一个简单的服务,提供保存和读取的接口。思来想去,我们的重点不在这里,于是改为本地模拟的形式。

网络实体

由于网络交互中有共同的状态、信息、数据等部分,这里进行抽取下。不太了解的同学可以看下我之前的一篇博客《一步步封装Retrofit + RxJava2》,这里有MVVM架构中Model层是怎样一步步生成的。

public class HttpResult<T> 
    public int status;
    public String msg;
    public T data;

模拟网络交互

仍然是两个方法,保存数据和获取数据,获取数据返回的是具体数据,
在保存数据中,返回的是数据本身。并且每个交互都做了延迟2秒的操作。

public class NetWork 

    private Application mApplication;

    public NetWork(Application application) 
        this.mApplication = application;
    

    public Observable<HttpResult<String>> saveData(final String content) 
        return Observable.create(
                new ObservableOnSubscribe<HttpResult<String>>() 
                    @Override
                    public void subscribe(ObservableEmitter<HttpResult<String>> e) 
                        SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE);
                        boolean success = sp.edit().putString("content", content).commit();
                        HttpResult httpResult = new HttpResult();
                        if (success) 
                            httpResult.status = 200;
                            httpResult.msg = "保存成功";
                         else 
                            httpResult.status = 5;
                            httpResult.msg = "保存失败";
                        
                        httpResult.data = content;
                        e.onNext(httpResult);
                        e.onComplete();
                    
                
        )
                .delay(2, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn浅谈iOS中的MVC MVP MVVM

Android : 跟我学Binder ---- 什么是Binder机制?

在MVVM架构Android中启动服务的正确位置是什么

Android中MVVM是什么?

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

在 MVVM 架构 Android 中启动服务的正确位置是啥