Android MVVM 模式介绍

目录

1.MVVM模式分为Model,View,ViewModel 

注意点

2.MVVM模式图

3.Android MVVM架构

4.Databinding框架

Note:

5.双向绑定使用到的注解

1)@InverseBindingAdapter

2)@InverseBindingMethod与@InverseBindingMethods

3)@InverseMethod

4)@Bindable

6.ViewModel 的生命周期

7.MVVM的优势和劣势

7.1优势

7.2劣势


1.MVVM模式分为Model,View,ViewModel 

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

注意点

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

2.MVVM模式图

3.Android MVVM架构

View
显而易见Activity/Fragment便是MVVM中的View,当收到ViewModel传递过来的数据时,Activity/Fragment负责将数据以你喜欢的方式显示出来。View还包括ViewDataBinding,上面中并没有体现。

ViewModel
ViewModel作为Activity/Fragment与其他组件的连接器。负责转换和聚合Model中返回的数据,使这些数据易于展示,并把这些数据改变即时通知给Actvity/Fragment。
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将会自动刷新。

4.Databinding框架

Databinding和MVVM的关系

MVVM是一种架构模式,DataBinding是一个实现数据和UI绑定的框架,是实现MVVM模式的工具。

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

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

Databinding常用方法

1).BindingAdapter注解设置自定义属性

public class TripleRadioRecyclerView extends RecyclerView {
...
@BindingAdapter("scrollListener")
public static void addOnScrollListener(TripleRadioRecyclerView recyclerView, IScrollListener listener) {
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            private boolean scrollByDragging;

            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                int position = ((TripleRadioRecyclerView) recyclerView).getSelection();
                if (RecyclerView.SCROLL_STATE_DRAGGING == newState) {
                    listener.onStartScroll(position);
                    scrollByDragging = true;
                } else if (RecyclerView.SCROLL_STATE_IDLE == newState) {
                    listener.onStopScroll(position);
                    scrollByDragging = false;
                }
            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                int position = ((TripleRadioRecyclerView) recyclerView).getSelection();
                listener.onScroll(position, scrollByDragging);
            }
        });
    }
...
}
public class TemperatureScrollViewModel {
...
    public IScrollListener scrollListener = new IScrollListener() {
        @Override
        public void onStartScroll(int position) {
            temperatureViewModel.setAdjusting(true, true);
            dragging = true;
            updateEnable();
        }

        @Override
        public void onScroll(int position, boolean fromUser) {
            Log.i(TAG, "onScroll: position=" + position + ", fromUser=" + fromUser);
            if (fromUser) {
                if (position != TemperatureScrollViewModel.this.position.get()) {
                    String temperature = transformer.getModelValue(position);
                    temperatureViewModel.setData(temperature);
                    Log.i(TAG, "onScroll: position=" + position + ", temperature=" + temperature);
                }
            }
        }
...
}

xml中使用自定义属性

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="com.databindingdemo.bean.HvacViewModel" />
        <variable
            name="hvac"
            type="HvacViewModel" />
    </data>



        <com.gwm.demo.hvac.widget.TripleRadioRecyclerView
            android:id="@+id/recycler_passenger_temperature"
            android:layout_width="wrap_content"
            android:layout_height="96dp"
            android:layout_below="@id/btn_passenger_tmp_inc"
            android:layout_margin="@dimen/button_margin"
            android:layout_toEndOf="@id/seek_bar_passenger_tmp"
            android:enabled="@{hvac.passengerTemperatureScrollViewModel.enable}"
            app:selection="@{hvac.passengerTemperatureScrollViewModel.position}"
            app:scrollListener="@{hvac.passengerTemperatureScrollViewModel.scrollListener}" />
</layout>   

自定义实现RecyclerView,scrollListener执行自定义@BindingAdapter("scrollListener")过程,在ViewMode中回调操作。

NOTE:没有类似方法,添加对应的方法
比如 app:xxx 属性 

  • 如果方法签名就是 app:xxx 属性里设定的值得话,直接定义 setXxx 方法即可。 
  • 如果方法签名像上面的 setPaddingLeft 一样,还需要把 View 自己也传入的话,那么除了要定义 setXxx 方法,还需要添加 @BindingAdapter(“android:xxx”)

 

public class MainActivity extends AppCompatActivity  {
    //用户头像
    private static final String URL_USER_PIC = "http://xxx/xx.jpg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        UserBean userBean = new UserBean(URL_USER_PIC, "张三", 24);
        binding.setUser(userBean);
    }
}

DataBinding动态更新数据的两种方式

1).BaseObservable
这个类也实现了字段变动的通知,在变量的getter上使用Bindable注解,并通过notifyPropertyChanged通知更新即可。

public class DoubleBindBean extends BaseObservable {

// 用 @Bindable 标记过 getxxx() 方法会在 BR 中生成一个 entry。 当数据发生变化时需要调用 //notifyPropertyChanged(BR.content) 通知系统 BR.content这个 entry 的数据
//已经发生变化以更新UI。

    private String content; //内容

    public DoubleBindBean(String content) {
        this.content = content;
    }

    @Bindable
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
        notifyPropertyChanged(BR.content); //通知系统数据源发生变化,刷新UI界面
    }
}

2).ObservableFiled
如果想要省时或者数据类的字段很少的话,可以使用ObservableFiled以及它的派生ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble,ObservableParcelable等。

public class DoubleBindBean2 {
    //变量需要为public
    public final ObservableField<String> username = new ObservableField<>();
}

Observable Collections
除了支持ObservableField,ObservableBoolean,ObservableInt等基础变量类型以外,当然也支持集合框架拉,比如:ObservableArrayMap,ObservableArrayList。使用和普通的Map、List基本相同.

3).双向绑定
以上两个说的都是单向绑定,数据的流向是单向的,下面我们说说双向绑定。
幸运的是,Android原生控件中,绝大多数的双向绑定使用场景,DataBinding都已经帮我们实现好了:可以参考包名androidx.databinding.adapters下实现了系统基本所有原生控件双向绑定的Adapter类

这意味着我们并不需要手动去实现复杂的双向绑定,以EditText为例,我们只需要通过@=(表达式)进行双向绑定:

<EditText
    android:id="@+id/etPassword"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={ viewModel.password }" />

相比单向绑定,只需要多一个=符号,就能保住View层和ViewModel层的状态同步了。

难点在哪?

双向绑定定义好之后,使用起来很简单,但是定义却稍微比单向绑定麻烦一些,即使原生控件的DataBinding已经帮助我们实现好了,对于三方的控件或者自定义控件,还需要我们自己实现。
我们已SwipeRefreshLayout为列,让我们来看看其双向绑定实现的方式:

package com.geespace.doublebinding

import android.util.Log
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout

/**
 * 
Description:
 */

object SwipeRefreshLayoutBinding {

  //先实现单向绑定
    @JvmStatic
    @BindingAdapter("swipeRefreshLayout_refreshing")
    fun setSwipeRefreshLayoutRefreshing(
        swipeRefreshLayout: SwipeRefreshLayout,
        newValue: Boolean) {
        Log.e("swipeBinding", "setSwipeRefreshLayoutRefreshing:$newValue")
        if (swipeRefreshLayout.isRefreshing != newValue) //不要忘了防止死循环!
      //保证,只有View状态发生了变更,才会去更新UI
            swipeRefreshLayout.isRefreshing = newValue
    }

    @JvmStatic
    @InverseBindingAdapter(
        attribute = "swipeRefreshLayout_refreshing",
        event = "swipeRefreshLayout_refreshingAttrChanged" //2 .匹配 1
    )
    fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
        swipeRefreshLayout.isRefreshing

    @JvmStatic
    @BindingAdapter(
        "swipeRefreshLayout_refreshingAttrChanged",  //1.注意默认是AttrChanged结尾
        requireAll = false)
    fun setOnRefreshListener(
        swipeRefreshLayout: SwipeRefreshLayout,
        bindingListener: InverseBindingListener?) {
        Log.e("swipeBinding","setOnRefreshingListener")

        if (bindingListener != null)
            swipeRefreshLayout.setOnRefreshListener {
                bindingListener.onChange()   //每当swipeRefreshLayout刷新状态被用户的      
                              //操作改变,我们都能够在这里监听到,
                              //并交给InverseBindingListener这个 信使 去通知DataBinding:

   
            }
    }
}

xml文件如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
        name="viewModel"
        type="com.geespace.doublebinding.BaseViewModel" />
    </data>

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/swipe"
        app:swipeRefreshLayout_refreshing="@={viewModel.refreshing }">

        <TextView
            android:layout_width="100dp"
            android:text="消失掉吧"
            android:id="@+id/txt_hide"
            android:padding="20dp"
            android:layout_height="100dp" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</layout>

refreshing参数如下:

class BaseViewModel :ViewModel(){
        var refreshing:MutableLiveData<Boolean> = MutableLiveData()
}

Activity的代码

class MainActivity2 :AppCompatActivity(){
    lateinit var swipeBinding:SwipeBinding
    lateinit var viewModel:BaseViewModel
    var handler:Handler=Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        swipeBinding=DataBindingUtil.setContentView(this,R.layout.swipe)

        viewModel=ViewModelProvider(this).get(BaseViewModel::class.java)

        swipeBinding.viewModel=viewModel
        swipeBinding.setLifecycleOwner=this //一定要设置lifeCycleOwner 否则LiveData双向绑定不起作用

      viewModel.refreshing.observe(this, Observer {
            Log.e("mainActivity2,","isRefreshing:${it}")
        })

        txt_hide.setOnClickListener {
           viewModel.refreshing.postValue(false)
        }

    }
}

Note:

上面有个地方需要注意下:

当使用LiveData进行双向绑定的时候 一定要记得调用binding.setLifeCycleOwner方法,否则LiveData数据改变的时候,View没法收到通知,切记!!可以想象到binding内部使用这个LifecycleOwner给liveData设置了监听。

下面就是这个方法的详细说明:

/**
     * Sets the {@link LifecycleOwner} that should be used for observing changes of
     * LiveData in this binding. If a {@link LiveData} is in one of the binding expressions
     * and no LifecycleOwner is set, the LiveData will not be observed and updates to it
     * will not be propagated to the UI.
     *
     * @param lifecycleOwner The LifecycleOwner that should be used for observing changes of
     *                       LiveData in this binding.
     */
    @MainThread
    public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
        if (mLifecycleOwner == lifecycleOwner) {
            return;
        }
        if (mLifecycleOwner != null) {
            mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
        }
        mLifecycleOwner = lifecycleOwner;
        if (lifecycleOwner != null) {
            if (mOnStartListener == null) {
                mOnStartListener = new OnStartListener(this);
            }
            lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
        }
        for (WeakListener<?> weakListener : mLocalFieldObservers) {
            if (weakListener != null) {
                weakListener.setLifecycleOwner(lifecycleOwner);
            }
        }
    }

5.双向绑定使用到的注解

1)@InverseBindingAdapter

1.作用于方法,方法须为公共静态方法。
2.方法的第一个参数必须为View类型,如TextView等
3.需要与@BindingAdapter配合使用

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface InverseBindingAdapter {

    String attribute();

    String event() default "";
}

attribute:String类型,必填,表示当值发生变化时,要从哪个属性中检索这个变化的值,示例:"android:text"
event: String类型,非必填;如果填写,则使用填写的内容作为event的值;如果不填,在编译时会根据attribute的属性名再加上后缀“AttrChanged”生成一个新的属性作为event的值,举个例子:attribute属性的值为”android:text”,那么默认会在”android:text”后面追加”AttrChanged”字符串,生成”android:textAttrChanged”字符串作为event的值.
event属性的作用: 当View的值发生改变时用来通知dataBinding值已经发生改变了。开发者一般需要使用@BindingAdapter创建对应属性来响应这种改变

2)@InverseBindingMethod与@InverseBindingMethods

1.@InverseBindingMethods注解用于标记类
2.@InverseBindingMethod注解需要与@InverseBindingMethods注解结合使用才能发挥其功效
3.@InverseBindingMethods需要与@BindingAdapter配合使用才能发挥功效
用法与示列:

@InverseBindingMethods({
        @InverseBindingMethod(type = SeekBar.class, attribute = "android:progress"),
})
public class SeekBarBindingAdapter {}

@InverseBindingMethod

@Target(ElementType.ANNOTATION_TYPE)
public @interface InverseBindingMethod {

    Class type();

    String attribute();

    String event() default "";

    String method() default "";
}

type:Class类型,必填,如:SeekBar.class
attribute:String类型,必填,如:android:progress
event:String类型,非必填,属性值的生成规则以及作用和@InverseBindingAdapter中的event一样。
method:String类型,非必填,对于什么时候填什么时候不填,这里举个例子说明:比如SeekBar,它有android:progress属性,也有getProgress方法【你没看错,就是getProgress,不是setProgress】,所以对于SeekBar的android:progress属性,不需要明确指定method,因为不指定method时,默认的生成规则就是前缀“get”加上属性名,组合起来就是getProgress,而刚才也说了,getProgress方法在seekBar中是存在的,所以不用指定method也可以,但是如果默认生成的方法getXxx在SeekBar中不存在,而是其他方法比如getRealXxx,那么我们就需要通过method属性,指明android:xxx对应的get方法是getRealXxx,这样dataBinding在生成代码时,就使用getRealXxx生成代码了;从宏观上来看,@InverseBindingMethod的method属性的生成规则与@BindingMethod的method属性的生成规则其实是类似的

3)@InverseMethod

作用于方法,
用于双向绑定
@InverseMethod 注解是一个相对独立的注解,不需要其他注解的配合就可以发挥它强大的作用,它的作用就是为某个方法指定一个相反的方法。它有一个String类型的必填属性:value,用来存放与当前方法对应的相反方法
正方法与反方法的要求:

  • 正方法与反方法的参数数量必须相同
  • 正方法的最终参数的类型与反方法的返回值必须相同
  • 正方法的返回值类型必须与反方法的最终参数类型相同。
    用列如下:
@InverseMethod("convertIntToString")
public static int convertStringToInt(String value) {
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        return -1;
    }
}
public static String convertIntToString(int value) {
    return String.valueOf(value);
}

4)@Bindable

该注解用于双向绑定,需要与 notifyPropertyChanged()方法结合使用
该注解用于标记实体类中的get方法或“is”开头的方法,且实体类必须继承BaseObserable.
示例用法

public class User extends BaseObservable {

    private String name;
    private int age;
    private String sex;
    private boolean isStudent;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(com.qiangxi.databindingdemo.BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(com.qiangxi.databindingdemo.BR.age);
    }

    @Bindable
    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
        notifyPropertyChanged(com.qiangxi.databindingdemo.BR.sex);
    }

    @Bindable
    public boolean isStudent() {
        return isStudent;
    }

    public void setStudent(boolean student) {
        isStudent = student;
        notifyPropertyChanged(com.qiangxi.databindingdemo.BR.student);
    }

    @Bindable({"name", "age", "sex", "isStudent"})
    public String getAll() {
        return "姓名:" + name + ",年龄=" + age + ",性别:" + sex + ",是不是学生=" + isStudent;
    }
}

@Bindable注解是用来干什么的?
使用@Bindable注解标记的get方法,在编译时,会在BR类中生成对应的字段,然后与notifyPropertyChanged()方法配合使用,当该字段中的数据被修改时,dataBinding会自动刷新对应view的数据,而不用我们在拿到新数据后重新把数据在setText()一遍,就凭这一点,dataBinding就可以简化大量的代码

6.ViewModel 的生命周期

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 LifecycleViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。

下图说明了 Activity 经历屏幕旋转而后结束时所处的各种生命周期状态。该图还在关联的 Activity 生命周期的旁边显示了 ViewModel 的生命周期。此图表说明了 Activity 的各种状态。这些基本状态同样适用于 Fragment 的生命周期。

通常在系统首次调用 Activity 对象的 onCreate() 方法时请求 ViewModel。系统可能会在 Activity 的整个生命周期内多次调用 onCreate(),如在旋转设备屏幕时。ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 Activity 完成并销毁。

7.MVVM的优势和劣势

7.1优势

1),使得M,V,VM的解耦更加彻底,在mvp模式中,p需要持有V的引用,才能去刷新UI,在MVVM模式中,View和Model使用databingding进行双向绑定,一方改变会直接通知另外一方,使得viewModel能专注于业务逻辑的处理,而不需要去关心UI刷新。
2),不会像MVC一样导致Activity中代码量巨大,也不会像MVP一样出现大量的View接口(Presente与View是通过接口进行交互的)。项目结构更加低耦合。

7.2劣势

1),数据绑定使得Bug很难被调试。
2),一个大的模块中,model也会很大,虽然使用方便了也很容易保证了数据的一致性,但是长期持有,不释放内存,就造成了花费更多的内存。
3),数据双向绑定不利于代码重用。客户端开发最常用的重用时View,但是数据双向绑定技术,让你在一个View都绑定了一个model,不同模块的model都不同,那就不能简单重用View了。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值