初步探索MVVM框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xzytl60937234/article/details/82430718

在Google I / O期间(去年),Google推出了包含LiveDataViewModelArchitecture Components,这有助于使用MVVM模式开发Android应用程序。

一、MVVM是什么

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构框架,它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化

二、MVVM功能图

这里写图片描述

这里写图片描述

这里写图片描述

我们看到view和viewModel是进行了数据绑定的。Google在2015年的已经为我们DataBinding技术。可参考【Android DataBinding完全解析】这篇文章。

三、在Android中MVVM的对应关系

View: 对应于Activity和XML,负责View的绘制以及与用户交互。

Model: 实体模型。

ViewModel: 负责完成View与Model间的交互,负责业务逻辑。

MVVM的目标和思想与MVP类似,利用数据绑定(Data Binding)、依赖属性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一个更加灵活高效的架构。

四、MVVM的优点

  • 低耦合。数据和业务逻辑处于一个独立的ViewModel中,ViewModel只需要关注数据和业务逻辑,不需要和UI或者控件打交道。UI想怎么处理数据都由UI自己决定,ViewModel不涉及任何和UI相关的事,也不持有UI控件的引用列表内容
  • 可重用性。*一个ViewModel可以复用到多个View中*。同样的一份数据,可以提供给不同的UI去做展示。对于版本迭代中频繁的UI改动,更新或新增一套View即可。如果想在UI上做A/B Testing,那MVVM是你不二选择。
  • 独立开发。由于View和ViewModel之间是松散耦合的:一个是处理业务和数据、一个是专门的UI处理。所以,完全由两个人分工来做,一个做UI(XML和Activity)一个写ViewModel,效率更高。
  • 单元测试。在MVVM中数据是直接绑定到UI控件上的(部分数据是可以直接反映出UI上的内容),那么我们就可以直接通过修改绑定的数据源来间接做一些Android UI上的测试。

五、如何构建MVVM应用框架

View

View层做的就是和UI相关的工作,我们只在XML、Activity和Fragment写View层的代码,View层不做和业务相关的事,也就是我们在Activity不写业务逻辑和业务数据相关的代码,更新UI通过数据绑定实现,尽量在ViewModel里面做(更新绑定的数据源即可),Activity要做的事就是初始化一些控件(如控件的颜色,添加RecyclerView的分割线),View层可以提供更新UI的接口(但是我们更倾向所有的UI元素都是通过数据来驱动更改UI),View层可以处理事件(但是我们更希望UI事件通过Command来绑定)。简单地说:View层不做任何业务逻辑、不涉及操作数据、不处理数据,UI和数据严格的分开。

ViewModel

ViewModel层做的事情刚好和View层相反,ViewModel只做和业务逻辑和业务数据相关的事,不做任何和UI相关的事情,ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,做的事情也都只是对数据的操作(这些数据绑定在相应的控件上会自动去更改UI)。同时DataBinding框架已经支持双向绑定,让我们可以通过双向绑定获取View层反馈给ViewModel层的数据,并对这些数据上进行操作。关于对UI控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件的处理统一化,为此我们通过BindingAdapter对一些常用的事件做了封装,把一个个事件封装成一个个Command,对于每个事件我们用一个ReplyCommand去处理就行了,ReplyCommand会把你可能需要的数据带给你,这使得我们在ViewModel层处理事件的时候只需要关心处理数据就行了,具体见MVVM Light Toolkit 使用指南的 Command 部分。再强调一遍:ViewModel 不做和UI相关的事。

Model

Model层最大的特点是被赋予了数据获取的职责,与我们平常Model层只定义实体对象的行为截然不同。实例中,数据的获取、存储、数据状态变化都是Model层的任务。Model包括实体模型(Bean)、Retrofit的Service ,获取网络数据接口,本地存储(增删改查)接口,数据变化监听等。Model提供数据获取接口供ViewModel调用,经数据转换和操作并最终映射绑定到View层某个UI元素的属性上。

ViewModel与View的协作

这里写图片描述

ViewModel由5部分组成:

  1. Context 上下文
  2. Model数据源 bean
  3. Data Filed 数据绑定
  4. Command 命令绑定
  5. ChildViewModel 子 ViewModel

六、实例

具体实现可参照:点击下载

开发者下载该项目,然后需要复制几个工具类到自己的项目中:
ActivityUtils.java
ViewModelHolder.java

然后我们开启android自带的DataBinding开关,再app下的build.gradle中android节点下添加如下代码:

dataBinding {
        enabled = true
    }

这样MVVM的环境就算配置好了。

我们来显示一个 hello world

首先我们创建我们的Fragment和ViewModel分别通过如下方法:

 @NonNull
    private TestOneFragment findOrCreateViewFragment() {
        TestOneFragment tasksFragment =
                (TestOneFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (tasksFragment == null) {
            // Create the fragment
            tasksFragment = TestOneFragment.newInstance();
            ActivityUtils.addFragmentToActivity(
                    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
        }
        return tasksFragment;
    }
private TestOneViewModel findOrCreateViewModel() { 
// In a configuration change we might have a ViewModel present. It's retained using the 
// Fragment Manager. 
@SuppressWarnings("unchecked") 
ViewModelHolder<TestOneViewModel> retainedViewModel = 
(ViewModelHolder<TestOneViewModel>) getSupportFragmentManager() 
.findFragmentByTag(TEST_ONE_VIEW_MODEL_TAG); 
if (retainedViewModel != null && retainedViewModel.getViewmodel() != null) { 
// If the model was retained, return it. 
return retainedViewModel.getViewmodel(); 
} else { 
// There is no ViewModel yet, create it. 
TestOneViewModel viewModel = new TestOneViewModel(getApplicationContext()); 
// and bind it to this Activity's lifecycle using the Fragment Manager. 
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), ViewModelHolder.createContainer(viewModel), 
TEST_ONE_VIEW_MODEL_TAG); 
return viewModel; 
} 
}

因为数据源改变了,XML的值自动改变,说明viewModel控制着XML,那就需要将XML和viewModel链接起来,同理我们有时候还需要获取代码层UI也就是Fragment的数据或者方法,此时也可以将XML和Fragment连接起来。

怎样连接呢?首先需要在XML声明ViewModel和Fragment变量,XML(test_one_fragment)代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="view"
            type="com.example.liumengqiang.mvvmoneproject.TestOneFragment"/>

        <variable
            name="viewModel"
            type="com.example.liumengqiang.mvvmoneproject.TestOneViewModel"/>

    </data>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="20sp"
            android:textColor="@color/colorAccent"/>
    </LinearLayout>
</layout>

然后在Fragment中,我们可以获取到自己创建的ViewModel和this(也就是Fragment本身啦)。这个ViewModel是从Activity中传递到Fragment中的。然后Fragment的onCreateView方法代码如下:

 @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.test_one_fragment, null, false);

        TestOneFragmentBinding bind = TestOneFragmentBinding.bind(view);
        //给XML设置View和ViewModel.
        bind.setView(this);
        bind.setViewModel(viewModel);

        viewModel.onStart();
        //TODO 获取网络数据

        return view;
    }

    public void setViewModel(TestOneViewModel viewModel) {
        this.viewModel = viewModel;
    }

此时在看ViewModel,由于我们要控制XML,所以需要继承自BaseObservable。
代码如下:

public class TestOneViewModel  extends BaseObservable{

    private Context context;

    public TestOneViewModel(Context context) {
        this.context = context;
    }

    public void onStart() {

    }

    /**
     * 获取数据从服务端
     */
    public void getDataFromServer() {

        //TODO 获取到数据之后,在设置textObservable的值,然后通过mNagivator回调给UI层,做进一步需求操作

    }
}

此时效果图如下:

基本数据类型

  • string
  • int
  • boolean
  • 引用数据类型
  • 图片设置

String的使用

首先开发者要在自己创建的ViewModel中声明:观察者:ObservableField, 由于想表示String类型,所以泛型时String。此时的被观察者就是我们数据源:String。
在ViewModel中声明如下:

 //String 类型
    public static ObservableField<String> textObservable = new ObservableField<>();

切记:必须是public 和 static;或者是public 和final,但是是 public static final ,这就牵涉到了 static 和final的区别了。

然后开发者可以在ViewModel中改变textObservable的值,如下:

 public void onStart() {
        textObservable.set("hello MVVM");
    }

此时效果图如下:


在此需要说一下,XML中的 “@{}”是该框架定义的,也算是格式吧,就是这样定义的。

int的使用
关于Int的使用跟String的用法一样的,只不过将泛型改变成Integer,或者将ObserVableField改成ObserVableInt。用法是一样的,只不过后者不需要添加泛型,因为它已经是一个表示Integer的观察者了。

需要注意的是XML中的TextView不能直接使用Integer类型,需要转化成String类型,框架中的XML可以这样定义“@{~~ + Integer}”
代码如下:

  <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="@color/colorAccent"
            android:text="@{~~ + viewModel.textObservable}"/>

boolean类型
关于Boolean类型,最常见的需求就是,给定一个Boolean,然后根据布尔值,View显示或者不显示。
首先在ViewModel中定义ObservableBoolean,代码如下:

public static ObservableBoolean observableBoolean = new ObservableBoolean();
  
  
  • 1

然后XML代码如下:

 <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:visibility="@{viewModel.observableBoolean ? View.VISIBLE : View.GONE}"/>

true显示,false不显示,从代码可以看出,我们使用了View.Visible,所以需要在XML中导入View包,所以在XML顶部添加包:

  <data>

        <!--导入View包-->
        <import type="android.view.View"/>

        <variable
            name="view"
            type="com.example.liumengqiang.mvvmoneproject.TestOneFragment"/>

        <variable
            name="viewModel"
            type="com.example.liumengqiang.mvvmoneproject.TestOneViewModel"/>

    </data>

这样开发者就可以在ViewModel中随意设置observableBoolean的值了。

引用数据类型
引用数据类型的话比较常见,也就是我们从服务器获取到的数据,塞进XML中,首先,需要创建model类,

public class TestOneModel {
    private String title;

    private String content;

    public TestOneModel(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

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

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

被观察者有了,接下来就是观察者了,开发者需要在ViewModel中创建一个观察者,代码如下:

   /*  /** 

     * 获取数据从服务端 

     */ 

    public void getDataFromServer() { 

        //TODO 获取到数据之后,在设置textObservable的值,然后通过mNagivator回调给UI层,做进一步需求操作 

        //假设已经从服务器获取到了数据 

        TestOneModel testOneModel = new TestOneModel(“title”, “content”); 

        testOneObservable.set(testOneModel); 

    }XML的代码也就是下面的了:

    /*  /** 

     * 获取数据从服务端 

     */ 

    public void getDataFromServer() { 

        //TODO 获取到数据之后,在设置textObservable的值,然后通过mNagivator回调给UI层,做进一步需求操作 

        //假设已经从服务器获取到了数据 

        TestOneModel testOneModel = new TestOneModel(“title”, “content”); 

        testOneObservable.set(testOneModel); 

    }XML的代码也就是下面的了:

 <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:textSize="20sp"
            android:textColor="@color/colorAccent"
            android:text="@{viewModel.testOneObservable.get().getTitle()}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:textSize="20sp"
            android:textColor="@color/colorAccent"
            android:text="@{viewModel.testOneObservable.get().getContent()}"/>

XML中,testOneObservable.get()的意思是获取到Observable的被观察者,也就是TestOneModel对象。

效果图如下:

图片设置

一般来说实际情况中,图片路径都是在服务器返回的model 中,所以我们在TestOneModel中添加一个成员变量:urlString, 然后赋值:http://img.zcool.cn/community/018d4e554967920000019ae9df1533.jpg@900w_1l_2o_100sh.jpg ,PS:这个路径是是网上找的

然后我们在XML添加如下:

 <ImageView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginTop="10dp"
            android:scaleType="fitXY"
            app:image_view="@{viewModel.testOneObservable.get().getUrlString()}"/>

注意:从代码中可以看出使用的不是src设置,而是外部属性image_view,这个image_view是随意命名的,然后我们新建一个DataBinding类,声明并实现设置图片。
代码如下:

public class TestOneDateBinding {

    @BindingAdapter("image_view")
    public static void setImageView(ImageView imageView, String urlString) {
        Glide.with(imageView.getContext()).load(urlString).into(imageView);
    }
}

效果图如下:

注意:这里BindingAdapter声明的名称必须和XML中图片的外部属性必须一致,这里也就是命名成了:image_view。接下来看下方法,这个方法里有两个参数,分别是Imageview和String,这两个是固定的,因为我们是给ImageView设置数据, 然后数据的类型是String类型,所以是固定的,比如之后会写到给RecyclerView设置数据,数据源是:List,那么这是的参数就是RecyclerView 和List<泛型>了。至于方法名,也就是根据开发者需求自己定义的了。

至此以上给基本数据类型和引用数据类型设置数据仅仅表示了MVVM的使用,当然,以前的开发者在UI的View层设置数据,还是有效的,使用步骤:
1:先在XML中声明TextView,

 <TextView
            android:id="@+id/tow_method_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:layout_marginTop="10dp"/>

2:在View(此处是Fragment)中,通过TestOneFragmentBinding的对象binding获取到XML中定义的TextView。

TextView towMethodText = bind.towMethodText;
        towMethodText.setText("我是第二种给XML设置数据的方法");

效果图如下:

从代码可以看出,Fragment中binding的成员变量就是XML中定义的id(去掉了中间的 _), 然后获得TextView对象,接下来的操作是不是熟悉了?

点击事件

关于点击事件,实际上跟接口回调差不多的,
1:开发者创建一个接口类 TestOneNagivator,

public interface TestOneNagivator {
    /**
     * TODO 添加点击事件或者是回调事件(连接ViewModel和UI层)
     */
    //点击事件
    void onClick();
}

2:在View层Activity 或者Fragment实现这个接口,我选择在Activity中实现了该接口:

 @Override
    public void onClick() {
        //TODO
        Toast.makeText(this, "点我干啥?", Toast.LENGTH_SHORT).show();
    }

3: 由于viewModel是控制器,需要该接口的实例,所以在Activity或者Fragment把接口实例传递给ViewModel

testOneViewModel.setNagivator(this);

4:同理在ViewModel接收该接口实例:

 private WeakReference<TestOneNagivator> mNagivator;
 public void setNagivator(TestOneNagivator nagivator) {
        this.mNagivator = new WeakReference<>(nagivator);
    }

5:然后开发者还需要在viewModel定义点击事件的方法:

    public void onTextClick() {
        if(mNagivator != null && mNagivator.get() != null) {
            //回调给UI层
            mNagivator.get().onClick();
        }
    }

6:在XML是调用ViewModel中定义的点击方法onTextClick,假如给图片添加点击事件:

   <ImageView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginTop="10dp"
            android:scaleType="fitXY"
            app:image_view="@{viewModel.testOneObservable.get().getUrlString()}"

            android:onClick="@{() -> viewModel.onTextClick()}"/>

此时点击图片就会弹出:点我干啥?
从XML中可以看出,@{() ->} 这个也是固定的,后面是点击事件的方法名,这也是MVVM点击事件的固定格式,这个不用纠结~

MVVM注意事项:

  1. ViewModel中观察者可以使ObservableArrayList等类型,开发者可以多试试其他类型的。
  2. 如果XML中用到了相应的数据,就需要导入相应的包,比如:在XML中使用了Fragment和ViewModel,就导入了对应的包,只不过又设置了一个别名而已:view 和 viewModel。 相同的还有Boolean的使用。

猜你喜欢

转载自blog.csdn.net/xzytl60937234/article/details/82430718