美文网首页MVP mvc mvvm
MVC、MVP、MVVM三个模式在Android的应用(MVVM

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

作者: LingoGuo | 来源:发表于2017-07-29 23:21 被阅读99次

    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(@{}表达式)表达式中使用

    相关文章

      网友评论

        本文标题:MVC、MVP、MVVM三个模式在Android的应用(MVVM

        本文链接:https://www.haomeiwen.com/subject/hkmqlxtx.html