MVVM 四大件

MVVM 四大件

一. DataBinding

1.1 引入

在对应Model的build.gradle中加入:

android {
    dataBinding {
        enabled = true
    }
}
复制代码

在布局时:

<?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>

 </data>

 <androidx.constraintlayout.widget.ConstraintLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity">

     <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Hello World!"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent" />

 </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

原有的布局被一个layout所包裹起来了,其中的<data>标签内,我们可以声明需要使用到的变量及其类型。

我们声明一个实体类:

public class User{
 private String userName;
 private String userCode;
 private String userGender;
 private String userTel;

 public User(String userName, String userCode, String userGender, String userTel) {
     this.userName = userName;
     this.userCode = userCode;
     this.userGender = userGender;
     this.userTel = userTel;
 }

 public String getUserName() {
     return userName;
 }

 public void setUserName(String userName) {
     this.userName = userName;
 }

 public String getUserCode() {
     return userCode;
 }

 public void setUserCode(String userCode) {
     this.userCode = userCode;
 }

 public String getUserGender() {
     return userGender;
 }

 public void setUserGender(String userGender) {
     this.userGender = userGender;
 }

 public String getUserTel() {
     return userTel;
 }

 public void setUserTel(String userTel) {
     this.userTel = userTel;
 }
}
复制代码

然后我们在页面的逻辑中,需要展示这个类的一些属性,我们在Databinding中,首先需要建立:视图和Model,即视图和数据之间的联系,让视图能够主动地感知到数据的 变动。

所以,我们在layout.xml中的data标签加入:

<variable    
       name="displayUserEntity"    
       type="com.example.mvvm1_databinding.User" />
复制代码

name可以自己填写,type则指定为:User实体类。

我们也可以使用<import>标签,将整个User包引入到<data>标签中,以便多个<variable>使用:

<data>

 <import type="com.example.mvvm1_databinding.User" />
 <variable
     name="displayUserEntity"
     type="User" />

 <variable
     name="createNewUserEntity"
     type="User" />
</data>
复制代码

如果我们导入了不同包下的User,可以用alias属性,指定别名。

 <data>

     <import type="com.example.mvvm1_databinding.User" />

     <import
         alias="UserOfOthers"
         type="com.example.mvvm1_databinding.others.User" />

     <variable
         name="displayUserEntity"
         type="User" />

     <variable
         name="createNewUserEntity"
         type="UserOfOthers" />
 </data>

复制代码

接下来,回到只有一个displayUserEntity和一个<variable>的情况,我们在真正的Layout中,创建几个Textview,用于数据的显示:

<?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="displayUserEntity"
         type="com.example.mvvm1_databinding.User" />
 </data>

 <LinearLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_margin="10dp"
     android:orientation="vertical"
     tools:context=".MainActivity">

     <TextView
         android:id="@+id/tv_user_code"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@{displayUserEntity.userCode}" />

     <TextView
         android:id="@+id/tv_user_name"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@{displayUserEntity.userName}" />

     <TextView
         android:id="@+id/tv_user_gender"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@{displayUserEntity.userGender}" />

     <TextView
         android:id="@+id/tv_user_tel"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@{displayUserEntity.userTel}" />
 </LinearLayout>
</layout>
复制代码

我们可以为其设置一个默认值,用于占位:android:text="{displayUserEntity.userName,default="预览占位"}"。:

在真正的布局文件中,我们可以获取到data中声明的属性相关的值。

Activity中,我们需要构建相关的实体类,模拟数据的填充:

class MainActivity : AppCompatActivity() {

 private lateinit var user: User

 override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     setContentView(R.layout.activity_main)
     //怎样去操作displayUserEntity?

     initData()
     doDataBinding()
 }

 private fun initData(){
     user = User("Jack", "17854124", "男", "400000000")
 }

 private fun doDataBinding() {
     val activityBinding: ActivityMainBinding =
         DataBindingUtil.setContentView(this, R.layout.activity_main)

     activityBinding.displayUserEntity = user
 }
}
复制代码

这样一来,我们代码中的数据能够被显示在界面上了,并且没有经过繁琐的findViewById(R.id.x).setText(value)

至此,我们就完成了数据从Model流向View层的流程。但是此时,我们新增一个按钮,声明事件:点击按钮改变user类的数值,并不能做到视图中的数据也被修改,即做不到双向数据绑定(Model即使被修改了,也无法做到View层同步修改)。此时解决的问题,仅仅是省略了繁琐的setText。

因此,如何做到双向数据绑定呢?

1.2 Observable

我们期望的是,当Model层的数据被修改时,视图层能够主动感知到数据的变化,从而自己去更新视图。这和观察者模式本身相契合。

1. BaseObservable

BaseObservable提供了两个方法:notifyChange()和notifyPropertyChanged(),前者会刷新所有的值域,后者则只更新对应注释的属性,我们可以在实体类中使用@Bindable标记一个希望被观察的对象。(如果是私有属性则标记在get方法上)

我们让Entity实体类继承自BaseObservable类。然后再某个属性上标记@Bindable:

我们让Entity实体类继承自BaseObservable类。然后再某个属性上标记@Bindable:

public class User extends BaseObservable {
 private String userName;
 private String userCode;
 private String userGender;
 private String userTel;

 public User(String userName, String userCode, String userGender, String userTel) {
     this.userName = userName;
     this.userCode = userCode;
     this.userGender = userGender;
     this.userTel = userTel;
 }

 @Bindable
 public String getUserName() {
     return userName;
 }

 public void setUserName(String userName) {
     this.userName = userName;
     notifyChange();//只要userName变化了,就刷新所有属性
 }

 @Bindable
 public String getUserCode() {
     return userCode;
 }

 public void setUserCode(String userCode) {
     this.userCode = userCode;
     notifyPropertyChanged(BR.userCode);//仅仅刷新界面上的userCode属性。
 }

 public String getUserGender() {
     return userGender;
 }

 public void setUserGender(String userGender) {
     this.userGender = userGender;
 }

 public String getUserTel() {
     return userTel;
 }

 public void setUserTel(String userTel) {
     this.userTel = userTel;
 }
}

// 注意,kotlin请在build.gradle中的plugins块内加入kapt的配置:
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}
// 否则@Bindable会报错

复制代码

这样一来,我们就对userName属性进行了监听,当useName属性发生改变时,视图就能主动感知了。

现在,我们添加几个方法,用于事件的更新,同样地使用Databinding,我们在原先的data标签处:

 <variable
            name="eventBinding"
            type="com.example.mvvm1_databinding.MainActivity.EventBinding" />
复制代码

新增一个eventBinding属性,该属性的type指向了MainActivity下的一个内部类:

    inner class EventBinding{
        fun tryLoadNewData(view:View){
            this@MainActivity.user.apply {
                userName = "Chris"
                userCode = "154751512"
                userGender = "女"
                userTel = "500000000"
            }
        }

        fun updateUserCode(view:View){
            //修改UserCode并不会触发全部数据的更新,因为是notifyPropertyChange
            this@MainActivity.user.userTel = "5555550"
            this@MainActivity.user.userCode = "1"
        }

        fun updateUserName(view:View){
            //修改Name会触发所有的修改,notifyChange
            this@MainActivity.user.userGender = "不男不女"
            this@MainActivity.user.userName = "abc"
        }

        fun resetData(view:View){
            this@MainActivity.initData()
        }
    }
复制代码

在布局文件中,新增几个Button,用于触发事件:

 <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="切换用户"
            android:onClick="@{eventBinding::tryLoadNewData}"
            tools:ignore="HardcodedText" />

        <Button
            android:id="@+id/btn_update_user_code"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="更新用户Code,只更新了自己"
            android:onClick="@{eventBinding::updateUserCode}"
            >

        </Button>

        <Button
            android:id="@+id/btn_update_user_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="更新用户Name,且会触发所有属性在界面上的更新"
            android:onClick="@{eventBinding::updateUserName}"
            >

        </Button>
复制代码

最后,我们在doDataBinding方法中,设置dataBinding的eventBinding属性即可:

    private fun doDataBinding() {
        activityBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)
        activityBinding.displayUserEntity = user
        eventBinding = EventBinding()

        activityBinding.eventBinding = eventBinding
    }
复制代码

这样一来,我们就可以看清楚notifyChangednotifyPropertyChanged的区别了。

我们也可以设置一个监听器:

        user.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
            override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
                Log.d("MVVM",propertyId.toString())
            }
        })
复制代码

//当有被@Bindable修饰的属性被修改时,此方法会被回调。打印的是被修饰变量的Id,而不是全部被更新的属性Id各打印一次。例如更新userTel userName userCode时,只有后面两个属性的值会被打印出来。

2. ObservableField

对一些基本数据类型的封装,也可以填入泛型参数。

3. ObservableCollections

用于替代原生的 ListMap,分别是 ObservableListObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新。在视图中,采用list[index]或者是map[key]来访问。

二. LiveData和ViewModel

在上文的DataBinding工具的介绍中,我们知道,我们可以很轻易地建立:View与视图Layout的连接,我们的后台数据利用Observe可以轻松地构建响应式的布局。实际上,这已经解决了MVC、MVP中的一个非常之繁琐的一个操作:findViewById、setText,我们只需要在Bean中的数据更新后,调用notify相关的方法即可更新到视图。这一切都基于DataBinding。Databinding本身更像是Activity和XML文件的黏合剂,使得View层的功能更加明确,不必再大量书写setText等修改视图的代码。

但是,这个步骤能不能再简单一点?回顾一下我们在面试过程中复习了无数遍的MVP和MVVM的区别,MVVM分三层,M、V、VM,M和V都好说,Presenter被替换成了VM,VM即ViewModel。

原先的Presenter任务繁重的原因很大程度上是因为Presenter作为”主持人”,需要持有V、M的引用,View层会利用Presenter去调用Model层的代码去取数据,而Model层又会使用Presenter的方法返回数据到视图。一个简单的功能,在Presenter中却要两个方法共同处理。

在引入VM后,得益于观察者模式,界面可以直接监听Model数据的变化,这样一来,VM层相对于原先的P层,进化了一点:只需要单向地处理View指派给Model层的任务了,这样减少了大量的代码。官方ViewModel类的出现很大程度上是为了规范对MVVM架构的实现。另外,它的一个派生类:AndroidViewModel可以获得运行时的Application。

我们直接继承自ViewModel即可使用:

//别忘了添加依赖
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

public class MyViewModel extends ViewModel {
    private final MutableLiveData<User> mUser = new MutableLiveData<>(new User(
            "Jack",
            "170000",
            "男",
            "12939487210"
    ));

    private final MutableLiveData<Integer> videoId = new MutableLiveData<>(-1);
    private final MutableLiveData<String> videoTitle = new MutableLiveData<>("Example");


    public MutableLiveData<User> getmUser() {
        return mUser;
    }
}


复制代码
//MainActivity中
private fun initViewModel() {    
    mMyViewModel = ViewModelProvider(this).get(MyViewModel::class.java);    
    //观察这个LiveData<User>,一旦产生属性变化,那么就回调到此处。   
    mMyViewModel.getmUser().observe(this, Observer {        
        Log.d("MVVM",it.toString())    
    })
}
复制代码

我们将user对象从Activity中搬运到了ViewModel中,这样一来,存储变量的任务就交给了ViewModel,而不是Activity。

三. Lifecycle

即生命周期,如果某个组件具有生命周期,那么可以实现接口:LifecycleOwner,即说明自己是一个生命周期的持有者,重写

@NonNull
Lifecycle getLifecycle();
复制代码

获得生命周期。

而lifecycle本身才是真正的生命周期,其中枚举了State的所有状态:

public enum State {
    DESTROYED,
    INITIALIZED,
    CREATED,
    STARTED,
    RESUMED;
}
复制代码

以及所有可能发生的相关ON事件:

public enum Event {
    ON_CREATE,
    ON_START,
    ON_RESUME,
    ON_PAUSE,
    ON_STOP,
    ON_DESTROY,
    ON_ANY
}
复制代码

我们可以自定义类继承自:LifecycleObserver来监听生命周期:

package com.example.lifecycle;

import android.util.Log;

import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;

import java.util.concurrent.Semaphore;

public class MyUtil implements LifecycleObserver {
    private String TAG = "MY_UTIL";
    private int count = 0;
    private Thread runner;

    private Semaphore semaphore = new Semaphore(1);//定义信号量

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    private void onCreate() throws InterruptedException {
        Log.d(TAG,"ON_CREATE");
        runner = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(500);
                    semaphore.acquire();//上锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(TAG, String.valueOf(count++));
                semaphore.release();//释放锁
            }
        });
        semaphore.acquire();
    }


    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    private void onResume(){
        Log.d(TAG,"ON_RESUME");
        if(!runner.isAlive()){
            runner.start();
        }
        semaphore.release();//释放掉锁
    }


    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    private void onPause() throws InterruptedException {
        Log.d(TAG,"ON_PAUSE");
        semaphore.acquire();//获得锁
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    private void onDestroy(){
        Log.d(TAG,"ON_DESTROY");
        runner.interrupt();
    }
}

//MainActivity.java的onCreate中
getLifecycle().addObserver(new MyUtil());

复制代码

这是一个很简单的例子,在onResume时,开启一个线程计数并输出,退出到桌面时,回调到onPause时,暂停计数输出,如果销毁则终止线程的执行。代码本身很简单,但是有一个要特别注意的点是,如果我们将addObserver的操作放在onStart()中。

package com.example.lifecycle;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        getLifecycle().addObserver(new MyUtil());//
    }
}

复制代码

而计数线程的声明是在onCreate的生命周期中,那么程序还能正常执行吗?直觉来说,应该是无法声明成功的,执行必然是失败的。然而:

输出.png

我们发现,onCreate()仍然被回调了。

这是因为LiveData具有的一个特性:粘性事件,那么何为粘性事件?
即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件。如果我们希望破除这一事件,首先我们得知道,任意一个LiveData都具有一个Version的属性,该属性记录了LiveData的版本。当一个新的监听产生时,那么就需要记录下事件发生时的数据的版本,和观察者之间进行比对,符合条件的进行派发,如果不符合条件的不进行派发,可以自行封装一个noStickyLiveData类,实现相关的操作。

四. 总结

在MVP中,View层持有P层引用,同时:

  1. 需要实现V通过P调用M的方法(请求数据
  2. 需要实现M通过P调用V的方法(更新视图
  3. 存储Activity自身的局部变量
  4. 简单用户交互管理、UI声明与管理(不涉及IO)
  5. 管理生命周期,合理调度,恢复数据。

在引入DataBinding + ViewModel + LiveData后:

  1. 需要实现V通过P调用M的方法(请求数据)- 只需要监听数据,数据基本处理交给VM层,VM层去Call Model层的方法。
  2. 需要实现M通过P调用V的方法(更新视图)- DataBinding + LiveData解决
  3. 存储Activity自身的局部变量 – ViewModel解决
  4. 响应用户输入事件、UI声明与管理(不涉及IO)- 这一点不变
  5. 管理生命周期,合理调度 – 至于恢复数据等操作,实际上ViewModel中存储LiveData能够很好地帮助我们处理数据,即使横屏重建也不会导致数据丢失。

所以,原先需要集中写在MainActivity.java中的五个功能点,现在只剩下1.5个:

  1. 简单用户交互管理(不涉及IO,响应OnClick事件或者简单地视图改变等等)
  2. 管理生命周期,合理调度。

进一步引入lifecycle以后,管理生命周期也可以剥离出来,这样一来,View层的职责就完完全全变成了:

  1. 响应用户输入事件、UI声明与管理(不涉及IO)

这是非常理想的一个View层模型。这样我们View层相关的类有:

//layout.xml
class MainActivity extends AppcompatActivity(){}
//1.响应用户输入 ->对应4

public class MyViewModel extends ViewModel{}
//1.存储变量 ->对应3
//2.请求Model层的方法,并在回调中修改LiveData的值,界面值由于DataBinding可以得到同步修改。 -> 对应1、2

public class MyLifecycleObserver implements LifecycleObserver{}
//1.监听生命周期 -> 对应5

复制代码

这样一来,相比较MVP层的P层,View层的职责更加清晰,P层的基于接口的代码量也大大下降(不需要再处理数据从M流向V层的接口方法了)。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享