MVP mvc mvvm

MVC、MVP、MVVM三个模式在Android的应用(MVVM

2017-07-29  本文已影响99人  LingoGuo

MVVM之前曾写过MVPMVC的文章,之后一直忙别的,把MVVM的文章给忘了。由于之前项目的图标已经没有了,只能换个界面,实现的功能与前面MVC、MVP项目的一样。

3.MVVM

MVVM其实跟前面讲的MVP差不多,如图所示:

MVP
屏幕快照 2017-07-29 上午9.55.40.png
MVVM
屏幕快照 2017-07-29 上午10.05.03.png

最大的区别就是DataBinding,先来看看各层的功能:
V层:.xml、Activity、Fragment,负责显示控件,通常还在Activity或者Fragment中获取ViewDataBinding的实例,将ViewModel的实例与这个实例绑定;

M层:与MVP的M层一样,存储数据、负责数据的业务逻辑,如网络请求、访问数据库,通常细分为model(数据的业务处理,如下面例子中获取经纬度和时间的逻辑代码部分)、bean(存储数据,如下面例子中的LocationEntity,存储经度、纬度、时间)

MV层:全称是ViewModel,类似于MVP的P层,但是通过DataBinding将V层与MV层绑定后,可以在MV层获取用户输入的数据和用户指令,调用M层获取数据,但是,当数据更新时,由于DataBinding,不需要在代码中对界面进行更新,这就是DataBinding的优势,ViewModel获取新数据后,V层可以自动更新

可以说DataBinding是MVVM的核心,怎么灵活利用DataBinding就很重要了,先上代码实现MVVM的设计模式,然后再来讲DataBinding的灵活利用。

功能:点击“查询”按钮显示当前经纬度和时间,当位置变化时自动刷新界面

Screenshot_20170729-114024.png Screenshot_20170729-114032.png Screenshot_20170729-114049.png

代码结构:

屏幕快照 2017-07-29 上午11.29.01.png

在build.gradle(Module:app)中开启dataBinding(android{}内部)

dataBinding{
        enabled=true
 }
屏幕快照 2017-07-29 上午10.21.28.png

manifests中声明以下三个权限
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

界面:


屏幕快照 2017-07-29 上午10.40.27.png

activity_main.xml(使用较新的ConstraintLayout)

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel"/>

    </data>
    <android.support.constraint.ConstraintLayout
         android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="当前经纬度"
            android:textSize="25dp"
            android:layout_marginLeft="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp" />

        <TextView
            android:id="@+id/latitude"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.location[`latitude`]}"
            tools:text="纬度"
            android:textAllCaps="false"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="24dp"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp" />

        <TextView
            android:id="@+id/longitude"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.location[`longitude`]}"
            tools:text="经度"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="24dp"
            app:layout_constraintTop_toBottomOf="@+id/latitude"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp" />

        <TextView
            android:id="@+id/date"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.location[`date`],default=`haha`}"
            tools:text="时间"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginTop="24dp"
            app:layout_constraintTop_toBottomOf="@+id/longitude"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp" />

        <Button
            android:background="@{1 < 3? @color/red : @color/white}"
            android:id="@+id/search"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="查询"
            android:onClick="@{(v)->viewModel.search(100)}"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp" />
    </android.support.constraint.ConstraintLayout>
</layout>


可以看到.xml与以往不一样,这是因为要支持DataBinding做出的改变,最外面嵌套了<layout>标签,在真正的布局之前添加了<data>标签作为这个布局的数据域,<data>里面用<variable>声明的变量可以在控件中使用,例如:

android:text="@{viewModel.location['date']}"
绑定变量viewModel名为location的域中key为“date”的value,location是ArrayMap<K, V>子类的一个实例,同时实现了观察者模式,也就是说数据变化(key为“date”的value发生改变)会引起绑定界面的变化(android:text内容的变化)

android:onClick="@{viewModel::search}"
事件处理的绑定,编译器会给这个View(这里是一个Button)注册点击监听器,当点击后调用变量viewModel的search(View v)方法。这里search的声明除了方法名其余必须与View.OnClickListener接口的抽象函数public void onClick(View v) 一致,如果不一致编译期间会报错。

MainActivity.java

package com.example.lingo.mvvmdemo.view;

import android.content.pm.PackageManager;
import android.databinding.DataBindingUtil;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
import com.example.lingo.mvvmdemo.R;
import com.example.lingo.mvvmdemo.databinding.ActivityMainBinding;
import com.example.lingo.mvvmdemo.util.AppConfig;
import com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel;

public class MainActivity extends AppCompatActivity {
    private MainActivityViewModel viewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding=DataBindingUtil.setContentView(this, R.layout.activity_main);
        viewModel=new MainActivityViewModel(MainActivity.this);
        binding.setViewModel(viewModel);

    }

    public void showToast(String text) {
        Toast.makeText(MainActivity.this,text,Toast.LENGTH_SHORT).show();
    }

    //6.0以后的系统请求权限的回调函数,无论哪种框架,这个方法只能在Activity中重载
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode== AppConfig.REQUEST_CODE) {
            for(int i=0;i<permissions.length;i++){
                if(grantResults[i]== PackageManager.PERMISSION_GRANTED){
                    showToast("权限已被允许");

                }else{
                    showToast("你拒绝了位置权限的申请");
                }
            }
        }
    }

}

ActivityMainBinding binding=DataBindingUtil.setContentView(this, R.layout.activity_main);
右边👉的表达式既给Activity设置了界面,同时也返回ViewDataBinding子类的实例,这里ActivityMainBinding与activity_main.xml存在联系,其实就是为带有<data>标签的activity_main生成一个类,这个类是ViewDataBinding的子类,名字就是将.xml每个被分隔的单词的首字母大写,后面加上Binding

屏幕快照 2017-07-29 下午2.56.29.png

在ActivityMainBinding.class(当然你得先编译)右键-Go To-Declaration,进入到ActivityMainBinding.java文件
看看生成的ActivityMainBinding的源码部分:

屏幕快照 2017-07-29 下午2.35.07.png

那一堆的public final看上去是不是有点眼熟,认真看不就是activity_main.xml布局中的控件和它们的android:id吗?现在你的代码中已经不需要findViewById了,取代它的是binding.date,binding.image等等就可以对相应的控件进行操作了,例如:

binding.date="2017年7月29号"
等价于
TextView date=(TextView).findViewById(R.id.date);
date.setText("2017年7月29号");

为binding绑定ViewModel实例:

viewModel=new MainActivityViewModel(MainActivity.this);
binding.setViewModel(viewModel);

为了更好理解,同样上源码(ActivityMainViewModel.java):

屏幕快照 2017-07-29 下午3.05.12.png

然后看回acitivity_mian的<variable>

屏幕快照 2017-07-29 下午3.07.13.png

所有声明的variable都会生成一个对应的setter和一个getter,例如:

  <variable
            name="str"
            type="String"/>

这里的str就是ViewModel(String类型)的名字,上面的viewModel也是ViewModel(MainActivityViewModel类型)取的一个名字,ViewModel可以是任意类型,作为MV层与V层绑定,这段代码将会在生成的ViewBinding的子类中有个void setStr(String)方法和String getStr()方法,调用对应的setter方法给binding绑定相应的ViewModel,实现V层与VM层的DataBinding

AppConfig.java

public class AppConfig {
    public static final int REQUEST_CODE=1;
}

LocationModel.java(与MVP一样,除了类型、接口等名字,其他没有变化)

package com.example.lingo.mvvmdemo.model;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

import com.example.lingo.mvvmdemo.bean.LocationEntity;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

/**
 * Created by lingo on 2017/7/29.
 */

public class LocationModel {
    private Context mContext;
    private LocationEntity mLocationEntity;//持有bean层的对象
    private LocationManager locationManager;
    private LocationListener locationListener;
    public LocationModel(Context mContext){
        this.mContext=mContext;
        mLocationEntity=new LocationEntity();
    }

    //请求位置数据的业务处理,部分代码可跳过,只需明白结构
    public void requestLocate(final OnLocationModelListener listener) {
        locationManager=(LocationManager)((mContext).
                getSystemService(Context.LOCATION_SERVICE));
        String locationProvider;
        List<String> providers = locationManager.getProviders(true);
        if(providers.contains(LocationManager.NETWORK_PROVIDER)){
            //如果是Network
            locationProvider = LocationManager.NETWORK_PROVIDER;
        } else if(providers.contains(LocationManager.GPS_PROVIDER)){
            //如果是GPS
            locationProvider = LocationManager.GPS_PROVIDER;
        }else{
            listener.fail(01,"没有可用的位置提供器");
            return ;
        }
        locationListener = new LocationListener() {


            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {

            }


            @Override
            public void onProviderEnabled(String provider) {

            }


            @Override
            public void onProviderDisabled(String provider) {

            }
            //当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
            @Override
            public void onLocationChanged(Location location) {
                if (location != null) {
                    String latitude;
                    if(location.getLatitude()>0){
                        latitude="N"+String.valueOf(location.getLatitude());
                    }else{
                        latitude="S"+String.valueOf(-location.getLatitude());
                    }
                    String longitude;
                    if(location.getLongitude()>0){
                        longitude="E"+String.valueOf(location.getLongitude());
                    }else{
                        longitude="W"+String.valueOf(-location.getLongitude());
                    }
                    mLocationEntity.setLatitude(latitude);
                    mLocationEntity.setLongitude(longitude);
                    SimpleDateFormat formatter = new SimpleDateFormat("yy年MM月dd日   HH:mm:ss");
                    Date date=new Date(System.currentTimeMillis());
                    mLocationEntity.setDate(formatter.format(date));
                    listener.success(mLocationEntity);
                }
            }
        };
        //获取Location后将数据存储在 mLocationEntity(bean层),回调调用 listener.success(mLocationEntity);
        try{
            Location location = locationManager.getLastKnownLocation(locationProvider);
            if (location != null) {
                String latitude;
                if(location.getLatitude()>0){
                    latitude="N"+String.valueOf(location.getLatitude());
                }else{
                    latitude="S"+String.valueOf(-location.getLatitude());
                }
                String longitude;
                if(location.getLongitude()>0){
                    longitude="E"+String.valueOf(location.getLongitude());
                }else{
                    longitude="W"+String.valueOf(-location.getLongitude());
                }
                mLocationEntity.setLatitude(latitude);
                mLocationEntity.setLongitude(longitude);
                SimpleDateFormat formatter = new SimpleDateFormat("yy年MM月dd日   HH:mm:ss");
                Date date=new Date(System.currentTimeMillis());
                mLocationEntity.setDate(formatter.format(date));
                listener.success(mLocationEntity);//回调
            }
            //监视地理位置变化
            locationManager.requestLocationUpdates(locationProvider, 3000, 1, locationListener);
        }catch (SecurityException e){
            listener.fail(02,e.getMessage());
        }catch(IllegalArgumentException e){
            listener.fail(03,e.getMessage());
        }
    }
    public interface OnLocationModelListener {
        void success(Object oj);
        void fail(int code,String message);
    }
}

LocationEntity.java

package com.example.lingo.mvvmdemo.bean;

/**
 * Created by lingo on 2017/7/29.
 */

public class LocationEntity {
    private String  latitude;
    private String longitude;
    private String date;
    public String getDate(){
        return date;
    }
    public void setDate(String date){
        this.date=date;
    }
    public String getLatitude() {
        return latitude;
    }

    public void setLatitude(String latitude) {
        this.latitude = latitude;
    }

    public String getLongitude() {
        return longitude;
    }

    public void setLongitude(String longitude) {
        this.longitude = longitude;
    }
}

MainActivityViewModel.java

package com.example.lingo.mvvmdemo.viewModel;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.databinding.ObservableArrayMap;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.Toast;

import com.example.lingo.mvvmdemo.bean.LocationEntity;
import com.example.lingo.mvvmdemo.model.LocationModel;
import com.example.lingo.mvvmdemo.util.AppConfig;
import com.example.lingo.mvvmdemo.view.MainActivity;


/**
 * Created by lingo on 2017/7/28.
 */

public class MainActivityViewModel  {
    public ObservableArrayMap<String, Object> location = new ObservableArrayMap<>();
    private Context mContext;//将引用MainActivity实例
    private LocationModel mLocationModel;//持有M层实例
    public MainActivityViewModel(MainActivity mContext) {
        this.mContext=mContext;
        this.mLocationModel = new LocationModel(mContext.getApplicationContext());//为了与V层解耦,传入
        location.put("latitude","纬度");
        location.put("longitude","经度");
        location.put("date","时间");
        //Application的Context实例
    }

    //当用户点击“查询”按钮时,该方法被调用
    //这一部分主要是权限的处理,6.0是一个分界点
    //6.0以前的权限处理和6.0以后的不一样,两种情况均要考虑,权限处理的细节
    public void search(View v) {
        int sdkInt = Build.VERSION.SDK_INT;
        if (sdkInt < Build.VERSION_CODES.M) {
            getLocate();
            return;
        }
        int permission = ContextCompat.checkSelfPermission
                (mContext, Manifest.permission.ACCESS_FINE_LOCATION);
        if (permission != PackageManager.PERMISSION_GRANTED) {//没有开启权限
            ActivityCompat.requestPermissions((MainActivity) mContext,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    AppConfig.REQUEST_CODE);//会在MainActivity中回调
        }else{
            getLocate();
        }


    }

    //调用M层加载数据,在回调的success方法中更新location(ObservableArrayMap<String, Object>),
    //activity_main.xml中控件的属性绑定了ObservableArrayMap的元素,而且ObservableArrayMap实现了观察者模式,
    //所以只要location一改变,界面就会自动改变,不用在添加更新UI的代码

    public void getLocate() {
        mLocationModel.requestLocate(new LocationModel.OnLocationModelListener() {
            @Override
            public void success(Object oj) {
                LocationEntity mLocationEntity=(LocationEntity)oj;
                location.put("latitude",mLocationEntity.getLatitude());
                location.put("longitude",mLocationEntity.getLongitude());
                location.put("date",mLocationEntity.getDate());
            }

            @Override
            public void fail(int code, String message) {
                showToast("错误代码:"+String.valueOf(code)+"错误信息:"+message);
            }
        });
    }
    public void showToast(String text) {
        Toast.makeText(mContext.getApplicationContext(),text,Toast.LENGTH_SHORT).show();
    }
}

到这里你应该清楚的意识到了MVP与MVVM的最大区别:VM层(相当于P层)获取到M层回调的数据后不需要添加更新UI的代码(MVP则需要),这是通过DataBinding将V层与MV层绑定,让V层控件与MV层数据(这些数据从M层获取)绑定,当MV层的这些数据通过调用M层得到更新时,V层自动更新控件

DataBinding灵活应用

你以为这样就完了吗?这样你就对MVVM满足了?MVVM的关键在于DataBinding,谷歌支持的DataBinding还有更多接下来要解锁的功能,灵活应用DataBinding能让你的代码更加简洁、更加爱MVVM。

控件属性绑定VM层的域或者getXX方法

当然这个VM层要申明在.xml的<data>标签中,如:

<variable
name="viewModel"
type="com.example.lingo.mvvmdemo.viewModel.MainActivityViewModel"/>

其实有一个默认的context变量没有显示声明,其实就是 rootView 的 getContext()方法的返回值,可以在@{}这样的binding表达式中使用

通常,我会在VM层对应的xxViewModel.java文件中添加要绑定的成员域,当然这些域的值还是要调用M层来更新,或者只添加对应的getXX方法。
例如.xml中的一个TextView控件

android:text="@{viewModel.firstName}"
当然也可以做一些操作,如android:text="@{viewModel.firstName+String.valueOf(1)}"或者@{viewModel.firstName+'1'}

更多的操作如三元运算等如下:

屏幕快照 2017-07-29 下午5.05.48.png

资源等也是支持的:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

那么我的xxViewModel.java文件中将有以下任何一种形式:
1.public String firstName;
2.private String firstName;//也可以不写
public String getFirstName(){}
3.public String firstName(){}//比较少
需要说明的是通常调用M层后,M层会回调MV层的一个方法,如上面代码中的onSuccess或者onFail方法,在onSuccess中调用setFirstName(更新后的值)或者使用表达式fisrtName=更新后的值,从而使得MV层与V层绑定的数据得到更新,然而采用上面任何一种形式都不会自动更新UI,因为没有实现观察者模式,那么怎么实现观察者模式呢?

你是否注意到了上面项目中的public ObservableArrayMap<String, Object> location,没错,这个类其实是实现了观察者模式的,所以当location的元素变化时对应控件会自动更新。但素,不是每个MV层的成员域都要求是集合,对于像String
firstName这样的成员域要怎么实现观察者模式以自动更新UI呢?

对于成员域,你可以这样:

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

怎么操作这个firstName呢?
使用ObservableField<T>的成员方法T get和set(T)

firstName.set("haha");
String name=firstName.get();

如果是基本的数据类型,安卓还提供了 ObservableDouble、 ObservableInt等等
问题又来了,如果我使用的是第二种形式,只有一个 public String getFirstName(){}没有申明firstName这个成员域呢?(当然为了能更新值你还是要有对应的setFirstName(String)方法)

做法:
首先让你的xxViewModel 继承 BaseObservable(其实ObservableField继承了BaseObservable),然后在getFirstName()方法添加注解@Bindable,最后在setFirstName里面添加 notifyPropertyChanged(BR.firstName);BR会自动生成,firstName会称为它的成员域(static final修饰)

dataBinding双向绑定

可以让viewModel数据域的改变直接反映在控件上,那是否也可以在控件改变时直接反映在数据域上?假如有一个Edittext,它的android:text绑定了viewModel的一个数据域,我们暂时取名为input吧,那在用户对这个Edittext输入时,是否可以用Edittext中用户输入的内容改变input的值呢?改变只需一点点:

andorid:text="@={viewModel.input}"

加个“=”,当用户输入时,用户输入的内容就会直接改变input的值了。

事件处理的绑定

事件处理的绑定有两种,无论采用哪一种都会给View注册一个对应的监听器,在监听器的方法中(覆盖抽象函数的那一个)进行事件处理,如android:onClick将会给这个View注册一个点击行为的监听器,重写public onClick(View v)这个抽象函数,在这个函数中处理事件

第一种就是项目使用的那种,叫做方法引用

android:onClick="@{viewModel::search}"

类似于正常的android:onClick,然后在Activity中写public void
方法名(View v){}这种形式,只是方法应该写在对应的xxViewModel.java文件中而不是Activity,这种大家应该很快就能上手

另外一种更加方便,名为监听者绑定,其实就是Lambda表达式的使用,大家可以看看第一种,方法的参数类型一定为View,但是如果采用监听者绑定这种方式,参数可以任意,只要返回值与对应监听器的抽象函数的返回值一样就可以。拿项目中的那个例子来说,其实点击“查询”Button后,search(View v)方法做的是权限的申请,申请成功后调用 getLocate()(在这个方法里调动M层),其实这里的参数v完全没有用,能不能不写呢?或者能不能改变参数的类型和数量?

采用第一种方式是不可以的,但是如果采用第二种方法,可以
更改search的参数类型

 public void search(int p) {
        int sdkInt = Build.VERSION.SDK_INT;
        if (sdkInt < Build.VERSION_CODES.M) {
            getLocate();
            return;
        }
        int permission = ContextCompat.checkSelfPermission
                (mContext, Manifest.permission.ACCESS_FINE_LOCATION);
        if (permission != PackageManager.PERMISSION_GRANTED) {//没有开启权限
            ActivityCompat.requestPermissions((MainActivity) mContext,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                    AppConfig.REQUEST_CODE);//会在MainActivity中回调
        }else{
            getLocate();
        }
        
    }

更改.xml

 <Button
            android:id="@+id/search"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="查询"
            android:onClick="@{()->viewModel.search(100)}"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp" />

ok了,实现的功能完全一样
@{()->viewModel.search(100)}中的100是我随便填的,这里使用到的是Lambda表达式(参数)->单行的函数体,可以看到search(int)返回值类型为void,跟public void
onClick(View v)一致。当然,这里省略了参数v(View类型),完整形式应该为:

android:onClick="@{(v)->viewModel.search(100)}"

这里需要注意的是如果你想要写参数,那你必须要写全,比如有些监听器的抽象函数是有多个参数的,像:

public static interface OnCheckedChangeListener {
        /**
         * Called when the checked state of a compound button has changed.
         *
         * @param buttonView The compound button view whose state has changed.
         * @param isChecked  The new checked state of buttonView.
         */
        void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
    }

这时候以下两种形式都可以:

<CheckBox android:layout_width="wrap_content" 
                      android:layout_height="wrap_content"
                      android:onCheckedChanged="@{(cb, isChecked) ->... }" />

<CheckBox android:layout_width="wrap_content" 
                      android:layout_height="wrap_content"
                      android:onCheckedChanged="@{() ->... }" />

甚至这种形式也是OK的

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

其中doSomething返回void

属性的Setters
1.自动的setter

对一个 attribute 来说,Data Binding 会尝试寻找对应的 setAttribute 函数。属性的命名空间不会对这个过程产生影响,只有属性的命名才是决定因素。举个例子:android:text="@{viewModel.firstName}",Data Binding则会寻找 setText(String),当然如果你的firstName是int类型, Data Binding则会寻找 setText(int),根据这个原则可以使用未用declare-styleable声明的自定义属性举个例子来说明:
在上面项目的activity_main.xml添加一个自定义view

 <com.example.lingo.mvvmdemo.view.Card
        android:layout_width="100dp"
        android:background="@color/red"
        android:layout_height="100dp"
        android:layout_marginTop="8dp"
        app:customSetter="@{viewModel.location[`latitude`]}"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

这里的app:customSetter是自定义的属性,但是没有在这个项目中没有用declare-styleable申明,那怎么办,不会报错吗?添加一个 public void setCustomSetter(String str) 就不会了,DataBinding会去找这个方法。

Card.java

package com.example.lingo.mvvmdemo.view;

import android.content.Context;
import android.graphics.Color;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.util.AttributeSet;
import android.widget.TextView;
import com.example.lingo.mvvmdemo.R;

/**
 * Created by lingo on 2017/7/29.
 */

public class Card extends ConstraintLayout{
    private TextView textView;
    public Card(Context context) {
        super(context);

    }

    public Card(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.card,this);
        textView=(TextView)findViewById(R.id.card_text);

    }
    public void setCustomSetter(String str) {
        textView.setText(str);
        setBackgroundColor(Color.GREEN);
    }
}

自定义的setter

看过有些文章称这个为DataBinding的最强技能,称"如果这个功能不能吸引你,那么恐怕没有什么能说服你使用 DataBinding了。"接下来就来了解一下这个被称为史上最酷的Android功能--BindingAdapter。
BindingAdapter只做一件事,就是将.xml中定义的属性值与对应的实现方法绑定在一起。

例如ImageView在XML中的android:src要求被赋予资源文件,可以我们往往从代码中动态的获取一个url,有没有办法直接在XML中通过属性设置为ImageView设置一个url,然后ImageView能显示这个url对应的图片?

其实说白了就是扩展ImageView在XML中的属性,让它更加强大和灵活
我们来看看要怎么做?
首先你可以通过attrs.xml(自己在values下建立)自定义一个属性或者就使用原有的android:src,无论是自定义属性还是原有的android:src,都要求能被赋予一个url(String类型)

step1:如果你选择扩展原有的android:src,可以跳过这一步;如果选择自定一个属性,比如命名为url,则首先要在attrs.xml添加以下代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyAttrs">
        <attr name="url" format="string" />
    </declare-styleable>
</resources>

step2:新建一个类,这里我命名为MyBindingAdapter.java:

public class MyBindingAdapter {
    private static Context mContext;

    public MyBindingAdapter(Context mContext) {
        this.mContext = mContext;
    }

    @BindingAdapter("url")
    public static void setImage1(ImageView view, String url) {
        //为了简单,这里假装根据url获取了对应的Bitmap实例
        Bitmap bitmap=BitmapFactory.decodeResource(mContext.getResources(), R.drawable.pic);
        view.setImageBitmap(bitmap);
    }
    @BindingAdapter("android:src")
    public static void setImage2(ImageView view, String url) {
        //为了简单,这里假装根据url获取了对应的Bitmap实例
        Bitmap bitmap=BitmapFactory.decodeResource(mContext.getResources(), R.drawable.pic);
        view.setImageBitmap(bitmap);
    }
}

其中setImage1对应自定义属性url,setImage2对应原有属性android:src,注意一个是"url",不需要命名空间,一个是"android:src“要求加上命名空间android,由于代码中使用了Context实例,可以在MainActivity.java中创建这个类的实例并传进去MainActivity.this

step3:MainActivityViewModel.java中添加一个名为url的成员域:

 public ObservableField<String> url=new ObservableField<>();
 public MainActivityViewModel(MainActivity mContext) {
       ...
        url.set("http://xxxxx");//随便填的url

step4:现在就是如果使用的问题了,在activity_main.xml中添加一个ImageView:

   <ImageView
            android:src="@{viewModel.url}"//也可以使用自定的url  app:url="@{viewModel.url}",所有自定义的属性均可用app:xxx
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
        />

ImageView显示了url对应的图片,当然为了方便,我并没有真的用了url去请求对应的图片,而是用了本地的资源图片,但是思路没有错。当然尽管扩展了android:src,也不会对原有的功能有影响, android:src="@drawable/pic"仍然可用。

当然,也可以将多个属性与一个实现方法绑定在一起
例如:

 @BindingAdapter(value = {"url", "placeHolder"}, requireAll = false)
    public static void setImageUrl(
            ImageView view, String url, int placeHolder) {
           。。。
    }

其中requestAll=false表示不需要同时设定两个属性也可以调用该方法
使用时:

 <ImageView
            app:url="@{viewModel.url}"
            app:placeHolder="@{1}"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
        />

当然两个属性可以不用同时设置,例如只设置 app:url="@{viewModel.url}"照样也会调用该方法,这是因为requireAll设置为false

至于事件属性,如android:onLayoutChange,可以参考谷歌安卓开发者文档,事件属性可以参考前面的事件处理的绑定或者直接在代码中设置。

其他

DataBinding最方便的三个技巧已经在上面一一介绍了,剩下的是一些比较简单的,如Custom Conversions如 android:background="@{isError ? @color/red : @color/white}",要提供一个从int转换为ColorDrawable的静态函数,这个静态函数用@BindingConversion修饰

 @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }

另外,可以在binding表达式中添加default表达式以设置默认值,这个默认值仅在预览窗口中可以看到,运行时看不到,类似有tools

android:text="@{viewModel.location[`date`],default=`haha`}"

haha仅在预览窗口可见,运行时不可见

关于自定义生成的Binding类的名字和位置还有<include>标签和<import>标签的使用可以参考谷歌的安卓开发者文档
注意:

<data>
    <import type="com.example.MyStringUtils"/>
    <variable name="user" type="com.example.User"/>
</data>
…
<TextView
   android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

import导进的类型中的静态属性和静态方法可以在属性的binding(@{}表达式)表达式中使用

上一篇 下一篇

猜你喜欢

热点阅读