MVVM&Android实践(二):动态绑定

第二部分:动态绑定

文章目录

    • 第二部分:动态绑定
      • Observable
      • ObservableFields
      • observable collections
        • `ObserableArrayList`
          • 初始化和赋值
          • 在xml中的使用
        • `ObserableArrayMap

DataBinding的强大之处在于,数据的变化会直接体现在界面上。如何达到这总效果呢?

DataBinding有三种数据变化的通知机制:Observable接口、ObservableFields接口以及observable collections。

Observable

实现android.databinding.Observable接口的类,可以允许开发人员添加一个监听器,用于监听对象属性变化。方便起见,DataBinding已经提供了一个继承了BaseObservable的类来实现这种机制。示例代码如下:

import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;

public class User extends BaseObservable {
    private String name;

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

    public void setName(String str) {
        this.name = str;
        notifyPropertyChanged(BR.name); // 通知界面数据刷新
    }

    public User(String name){
        this.name = name;
    }
}

代码中,有四个重点:

  • BaseObservable:接口android.databinding.Observable的实现,为动态绑定提供基础,如notifyPropertyChanged函数就来自于该类。
  • @Bindable:动态绑定的注解类,实现动态绑定的基础,用 @Bindable 标记过 getter 方法会在 BR 中生成一个 entry
  • notifyPropertyChanged:通过调用该函数,通知系统 BR.name 这个 entry 的数据已经发生变化,需要更新 UI。
  • BR:编译阶段生成的一个类,类似于Android的R类,暂时无需关心。

对应的xml文件:


<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>
        <import type="com.dali.mvvmdemo.User"/>
        <variable
            name="person"
            type="com.dali.mvvmdemo.User" />
    data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{person.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
    androidx.constraintlayout.widget.ConstraintLayout>
layout>

xml文件中,比较特别的就是:android:text="@{person.name}"这里的name在User类中是一个私有属性的变量,但在DataBinding框架的加持下,却能自由使用,你猜是怎么实现的呢?

来调用试试:

    private User user = new User("SuperLi");
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setPerson(user);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000 * 3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName("MrHeLi");
            }
        }).start();
    }

应用启动三秒钟后,界面上的TextView将会从显示SuperLi变为MrHeLi

这就是第一种动态绑定,我们来小结一下:

  • 继承BaseObservable对象。
  • 在需要动态绑定的成员属性的getter函数上添加@Bindable注解。
  • 在需要动态绑定的成员属性的setter函数中手动调用notifyPropertyChanged通知UI刷新数据。

ObservableFields

前一种绑定方式,需要改动的代码太多了。DataBinding框架定义了一些列基础类型,来使一个Object变得observable。这就是ObservableFields,它包含一些列和基础数据类型对应的observable类,以及ObservableField类。见下表:

基础数据类型 集合 其它类
ObservableBoolean ObservableArrayList ObservableField
ObservableByte ObservableArrayMap ObservableParcelable
ObservableChar ObservableMap
ObservableDouble ObservableList
ObservableFloat
ObservableInt
ObservableShort
ObservableLong

将上述数据类型放在我们Model中使用,动态绑定会显得非常简洁:

import androidx.databinding.ObservableField;
 
public class Employ {
    public final ObservableField<String> mName = new ObservableField<>();

    public Employ(String name) {
        mName.set(name);
    }
}

没错,就这样,没有setter、getter。只是改变ObservableXXXd对象的值时,需要改用mName.set(T)函数和mName.get()函数。xml文件和前面的也没什么不同:


<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="employ"
            type="com.superli.mvvmdemo.Employ" />
    data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{employ.mName}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
    androidx.constraintlayout.widget.ConstraintLayout>
layout>

主函数调用:

    private Employ employ = new Employ("张三");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		// ...
        binding.setEmploy(employ);
        // ...
    }

observable collections

这里就说一下简单并且常用的集合类型ObserableArrayListObservableArrayMap,其它的类型请自行探索。它们的底层数据结构都是动态数组,存取数据的逻辑本质也就是维护动态数组。本文的初衷不是研究具体实现,源码方面就不展开了。

对于这两个类,它们的值都是通过键值对的形式提供,下面详细看看它们的使用。

ObserableArrayList

对于该类,搜索了一下各类技术博客,发现大家都在使用一套让人一头雾水的代码。本人行文尽量不要多余代码,让初通Android的人都能看懂。

初始化和赋值
ObservableArrayList<Object> obserUser = new ObservableArrayList<>();
obserUser.add("This is the first data"); // 填加一条数据
obserUser.add("This is the second data");
obserUser.set(1, "hahhah"); // 将第二个位置的数据改为"hahhah"
binding.setObservableUser(obserUser); // 绑定数据

ObserableArrayList常用的函数有:

返回值 原型 说明
boolean add(T object) 添加一条数据
boolean void add (int index, T object) 向index插入一条数据,index后的数据依次向后移一位
boolean addAll(Collection collection) 将集合数据整体添加进对象
boolean addAll (int index, Collection collection) 在指定位置插入集合
void clear() 清除所有数据
T remove(int index) 删除指定index的数据

当然,不止这些,想要了解更多,请移步

官网

或者自行查看源码。

在使用ObserableArrayList进行数据的增删改查时,请注意你要操作的当前Index是否真是存在,如果不存在将会发生运行时异常。Index随着数据的添加(调用add(T object))函数而自增,所以,在操作Index前,请判断该Index是否存在。

在xml中的使用

先看代码:


<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>
        
        <import type="androidx.databinding.ObservableArrayList"/>
        <variable
            name="observableUser"
            type="ObservableArrayList<Object>" />
    data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{observableUser[1]}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

    androidx.constraintlayout.widget.ConstraintLayout>
layout>

这段xml代码中,有如下几点需要注意:

  1. 标签导入ObservableArrayList包:前面在使用String时,我们并没有用到标签,是因为String类型默认已经导入。但ObservableArrayList并没有包含在默认导入的列表中,我们需要手动导入。

  2. 声明ObservableArrayList变量名为observableUsertype="ObservableArrayList<Object>"这个地方有两个在Android中不常用的符号<>,它们分别时xml中间中的<>符号,替换一下就是:type="ObservableArrayList",是不是明白了?

    因为<>两个符号被xml用作标签的语法符号,所以其它需要的地方就只能用对应的转移字符了。记住:<>分别时<>的转义字符。

  3. android:text="@{observableUser[1]}":这行代码表示了ObservableArrayList类型对象的典型使用方式,通过index取值,如果你知道它底层数据结构是数组,就能理解,这完全就是数组的取值方式。

  4. 还算是挺简单的吧。

    ObserableArrayMap

    ObserableArrayMap的使用和ObserableArrayList非常类似,只不过在赋值和取值方式上,稍有差别罢了。后文重复的就不多说,只说差一点。

    初始化和赋值
    ObservableArrayMap<String, String> mapUser = new ObservableArrayMap<>();
    mapUser.put("name", "SuperLi"); // 赋值,这个和普通map没啥区别
    binding.setMapUser(mapUser);
    

    ObserableArrayMap有大量增删改查的函数供开发者使用,列举一些常见的:

    返回值 原型 说明
    V put(K k, V v) 增加一条记录
    void clear() 清除所有记录
    V remove(K k) 删除key为k的记录
    void removeAt(int index) 删除位于index位置的记录
    V remove(Object k) 删除key为k的记录
    V setValueAt(int index, V value) 修改位于index的记录
    int indexOfKey(@Nullable Object key) 查询key的index

    其它就不展开了,有需要请去官网或者源码看。
    官网链接

    在xml中的使用
    
    <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>
            <import type="androidx.databinding.ObservableArrayMap"/>
            <variable
                name="mapUser"
                type="ObservableArrayMap<String, String>" />
        data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">
    
            <TextView
                android:id="@+id/tv_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text='@{mapUser["name"]}'
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>
        androidx.constraintlayout.widget.ConstraintLayout>
    layout>
    

    多的不说,只说ObservableArrayMap对象如何取值:

    android:text='@{mapUser["name"]}'
    

    同样是数组的操作方式,只是把数组的指针变成了Map的key即可。

    Fragment中的绑定

    Fragment和Activity绑定xml文件,都需要使用DataBindingUtil类,只不过调用的函数有点区别:

    // Activity的绑定,通过setContentView函数实现
    final CustomBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    // Fragment的绑定通过inflate函数实现
    FragmentViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_view, null, true);
    

    其它的区别不大。为了文章的完整性,这里也把一个可以完整运行的Fragment绑定代码贴出来:

    Activity的布局文件activity_main.xml:

    
    <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 class="CustomBinding">
        data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">
            <FrameLayout
                android:id="@+id/fg_container"
                android:layout_width="100dp"
                android:layout_height="100dp"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>
        androidx.constraintlayout.widget.ConstraintLayout>
    layout>
    

    Activity代码MainActivity.java

    public class MainActivity extends AppCompatActivity {
        private FragmentManager mFragmentManager = getSupportFragmentManager();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            final CustomBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
            MyFragment myFragment = new MyFragment();
            FragmentTransaction transaction = mFragmentManager.beginTransaction();
            transaction.add(binding.fgContainer.getId(), myFragment);
            transaction.commit();
        }
    }
    

    Fragment布局文件fragment_view.xml

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"
            android:orientation="vertical">
        </LinearLayout>
    </layout>
    

    Fragment代码MyFragment.java:

    public class MyFragment extends Fragment {
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            FragmentViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_view, null, true);
            return binding.getRoot();
        }
    }
    

    RecyclerView中的绑定

    为了更加直观的对比,先来回顾一下RecyclerView的基本用法。

    RecyclerView基础用法

    Android基础包中不包含RecyclerView,需要添加额外的依赖。只需要在项目的build.gradle文件中加入:

    dependencies {
    	// ...
        implementation 'androidx.recyclerview:recyclerview:1.1.0'
        // ...
    }
    

    这样就能在布局文件中,使用RecyclerView了:

    主布局文件:recycler_activity.java:

    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    LinearLayout>
    

    Item布局文件: item_recycler.xml:

    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/tv_show"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:hint="1"
            android:textColor="@color/colorAccent" />
    LinearLayout>
    

    Adataper&ViewHolder:NumerAdapter.java:

    public class NumberAdapter extends RecyclerView.Adapter {
    
        private ArrayList<String> mData;
    
        public NumberAdapter(ArrayList<String> data) {
            mData = data;
        }
    
        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycler, parent, false);
            return new NumberViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            ((NumberViewHolder)(holder)).numberView.setText(mData.get(position));
        }
    
        @Override
        public int getItemCount() {
            return mData.size();
        }
    
        class NumberViewHolder extends RecyclerView.ViewHolder {
            public TextView numberView;
            public NumberViewHolder(@NonNull View itemView) {
                super(itemView);
                numberView = itemView.findViewById(R.id.tv_show);
            }
        }
    }
    

    Activity: RecyclerActivity.java:

    public class RecyclerActivity extends Activity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.recycler_activity);
    
            RecyclerView recycler = findViewById(R.id.recycler);
            LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
            recycler.setLayoutManager(layoutManager);
    
            NumberAdapter numberAdapter = new NumberAdapter(generateDatas());
            recycler.setAdapter(numberAdapter);
        }
    
        private ArrayList<String> generateDatas() {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < 100; i ++) {
                list.add(String.valueOf(i));
            }
            return list;
        }
    }
    

    本文的主题不是RecyclerView,所以就不作过多解释了。下面看看,如何使用DataBinding框架改造上面的代码。

    RecyclerView&DataBinding

    我们使用DataBinding的目的之一,其实就是将Model和界面绑定。放在RecyclerView上也一样,代码如下:

    主布局文件:recycler_activity.java:

    在这里,我们将Java代码中setAdapter的方式为RecyclerView设置adapter,改为databinding的方式。

    
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:adapter="@{adapter}"/>
        LinearLayout>
    
        <data class="NumberBinding">
    
            <import type="com.superli.mvvmdemo.recycler.NumberAdapter" />
    
            <variable
                name="adapter"
                type="NumberAdapter" />
        data>
    layout>
    

    这里需要注意的代码是:

    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:adapter="@{adapter}"
    
    • xmlns:app: 表示这里需要用到自定义的命名空间
    • app:adapter:表示,当前控件命名空间下,有一个叫adapter的属性,并给它赋值为"@{adapter}"

    Item布局文件: item_recycler.xml:

    同样将Java代码设置Text的方式,改为DataBinding的方式。

    
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/tv_show"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:hint="1"
                android:text="@{value}"
                android:textColor="@color/colorAccent" />
    
        LinearLayout>
    
        <data class="ItemView">
    
            <variable
                name="value"
                type="String" />
        data>
    layout>
    

    Adataper&ViewHolder:NumerAdapter.java:

    public class NumberAdapter extends RecyclerView.Adapter {
        private ArrayList<String> mData;
    
        public NumberAdapter(ArrayList<String> data) {
            mData = data;
        }
    
        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            ItemView view = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
                                                    R.layout.item_recycler, parent, false);
            return new NumberViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            ItemView binding = ((NumberViewHolder) (holder)).getBinding();
            binding.setValue(mData.get(position));
            // 立刻执行绑定,强制刷新界面,防止出现数据错位
            binding.executePendingBindings();
        }
    
        @Override
        public int getItemCount() {
            return mData.size();
        }
    
        class NumberViewHolder extends RecyclerView.ViewHolder {
            private ItemView itemView;
            public NumberViewHolder(@NonNull ItemView view) {
                // 这里需要通过xml的绑定类对象,通过getRoot函数将具体的View控件传递给父类。
                super(view.getRoot()); 
                this.itemView = view;
            }
    
            public ItemView getBinding() {
                return itemView;
            }
        }
    }
    

    需要注意的地方,用注释提醒。

    Activity: RecyclerActivity.java:

    public class RecyclerActivity extends Activity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            NumberBinding binding = DataBindingUtil.setContentView(this,
                                                                 R.layout.recycler_activity);
    
            LinearLayoutManager layoutManager = new LinearLayoutManager(this,
                                                       LinearLayoutManager.VERTICAL, false);
            binding.recycler.setLayoutManager(layoutManager);
            NumberAdapter numberAdapter = new NumberAdapter(generateDatas());
            binding.setAdapter(numberAdapter);
        }
    
        private ArrayList<String> generateDatas() {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < 100; i ++) {
                list.add(String.valueOf(i));
            }
            return list;
        }
    }
    

    RecyclerViewDataBinding结合,总体来讲,改变不大。此时,Medel和View虽然绑定在了一起,但如果我们随后改变Model,View并不会随之改变。此时,我们可以通过可观察类型来修改我们的Model即可。为了向这个方向改造,我们专门来谢一节。

    Model动态绑定

    RecyclerView&DataBinding一节代码的基础上,我们需要对Model相关的地方,做一点修改:

    Item布局文件: item_recycler.xml:

    变动不大,只是将value的类型改为可观察的数据类型。

    
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/tv_show"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:hint="1"
                android:text="@{value}"
                android:textColor="@color/colorAccent" />
    
        LinearLayout>
    
        <data class="ItemView">
    
            <variable
                name="value"
                type="androidx.databinding.ObservableField<String>" />
        data>
    layout>
    

    Adataper&ViewHolder:NumerAdapter.java:

    主要将mData的数据类型,从ArrayList改为ArrayList>

    public class NumberAdapter extends RecyclerView.Adapter {
        private ArrayList<ObservableField<String>> mData; // 修改mData数据类型
    
        public NumberAdapter(ArrayList<ObservableField<String>> data) {
            mData = data;
        }
    
        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                                                          int viewType) {
            ItemView view = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_recycler,
                                                    parent, false);
            return new NumberViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            ItemView binding = ((NumberViewHolder) (holder)).getBinding();
            binding.setValue(mData.get(position));
            // 立刻执行绑定,强制刷新界面,防止出现数据错位
            binding.executePendingBindings();
        }
    
        @Override
        public int getItemCount() {
            return mData.size();
        }
    
        class NumberViewHolder extends RecyclerView.ViewHolder {
            private ItemView itemView;
            public NumberViewHolder(@NonNull ItemView view) {
                // 这里需要通过xml的绑定类对象,通过getRoot函数将具体的View控件传递给父类。
                super(view.getRoot()); 
                this.itemView = view;
            }
    
            public ItemView getBinding() {
                return itemView;
            }
        }
    }
    

    Activity: RecyclerActivity.java:

    同样是修改数据类型,并且在主函数中创建了一个子线程,模拟Model的动态改变,用于测试。

    public class RecyclerActivity extends Activity {
        ArrayList<ObservableField<String>> mList = new ArrayList<>(); // 数据类型修改
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            NumberBinding binding = DataBindingUtil.setContentView(this, R.layout.recycler_activity);
    
            LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
            binding.recycler.setLayoutManager(layoutManager);
    
            NumberAdapter numberAdapter = new NumberAdapter(generateDatas());
            binding.setAdapter(numberAdapter);
    
            new Thread(new Runnable() { // 开一个子线程,用于测试
                @Override
                public void run() {
                    Log.i("HHHHH", "run run run");
                    try {
                        Thread.sleep(1000 * 3); // 先睡三秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i = 0; i < mList.size(); i++) { // 修改所有数据
                        mList.get(i).set(String.valueOf(mList.size() - i));
                    }
                }
            }).start();
        }
    
        private ArrayList<ObservableField<String>> generateDatas() {
            for (int i = 0; i < 100; i ++) {
                mList.add(new ObservableField<>(String.valueOf(i)));
            }
            return mList;
        }
    }
    

    这样改造完后,RecyclerView的界面就会自动跟随Model改变而改变,不再需要我们在代码中手动刷新RecyclerView了,这一点很赞。

    双向绑定

    前面所说的Model绑定方式,都是单向的。所谓的单向,是Model发生改变,可以触发View层的改变。但View层的改变,却无法影响Model

    双向绑定的一个典型的应用场景是:在一个EditText中,输入字符后,传统的做法是注册EditText的通知接口,并在接口中通过EditText.getText().toString()的方式获取用户输入的字符。而双向绑定,可以让EditText中用户输入的字符,直接反应到Model中。

    双向绑定非常简单,只需要在单向绑定的基础上,做一点小修改:

    @{employ.mName, default=`Stephen`}
    
    @={employ.mName, default=`Stephen`}
    

    来看看具体代码:

    布局文件:edit_main.xml:

    使用**@={employ.mName, default=Stephen},使View层的修改,可以同步到Model**中。

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <EditText
                android:id="@+id/et_employ"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:text="@={employ.mName, default=`Stephen`}" />
    
        </LinearLayout>
    
        <data class="EditBinding">
            <variable
                name="employ"
                type="com.superli.mvvmdemo.Employ" />
        </data>
    </layout>
    

    Model类 :Employ.java:

    使用可观察对象,让Model数据发生改变后,可以通知到View层。

    public class Employ {
        public final ObservableField<String> mName = new ObservableField<>();
        public Employ(String name) {
            mName.set(name);
        }
    }
    

    Activity :EditTextActivity.java

    public class EditTextActivity extends Activity implements TextWatcher {
        private Employ employ = new Employ("Kevin");
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            EditBinding binding = DataBindingUtil.setContentView(this, R.layout.edit_main);
            binding.setEmploy(employ);
            binding.etEmploy.addTextChangedListener(this);
        }
    
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    
        }
    
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
    
        }
    
        @Override
        public void afterTextChanged(Editable s) {
            Log.i("EditTextActivity", "employ.name = " + employ.mName.get());
        }
    }
    

    运行起来了,在EditText的监听事件中,直接使用Model提取最新数据。使得数据提取变得更加方便。

    在这个地方,初看用不用DataBinding代码上区别不大,但你细品…

    使用DataBinding双向绑定的方式,让程序员在编码工作中,完全将Model层和View层隔离开。使程序员的视线从Model、View、ViewModel三者之间切换降维成了在Model和ViewModel或者View和ViewModel两者之间切换。就像机械制图教授原本布置三维制图的作业,改为画一张二维图就可以,传说中的降维打击啊,就问你开不开心。

    函数or事件绑定

    在这里,事件绑定和函数绑定其实说的是一个事情。事件更强调驱动性,但它本身也是通过函数实现的。事件是特殊的函数。为了区分这一点,这一部分,也分开来说。

    对于双向绑定的例子来说,通过addTextChangedListener添加了监听,以实时触发获取Model数据,虽说能够实现,但这样的做法非常不优雅,也不DataBinding

    不管是自定义还是基础控件的各类事件,都可以通过DataBinding的事件绑定机制绑定。而要实现这一点,我们只需要做三件事情即可:

    1. 声明与正常处理监听函数签名(signature)相同的函数。
    2. 在XML文件的控件中,绑定第一步申明的函数。
    3. 在代码中将函数的对象与XML文件中的变量进行绑定。

    签名绑定

    比如,现在有一个点击事件需要绑定。按步骤需要这么做:

    1. 点击事件监听函数原型为:

      public interface OnClickListener {
      	void onClick(View v); // 点击事件监听器的函数原型
      }
      

      那么我需要在我的ViewModel中,根据该函数的签名,定义一个相同签名的函数,比如这样:

      public class Employ {
          public void onEmployClick(View v) { // 相同签名的函数
              Log.i(TAG, "onClick");
          }
      }
      

      如果是长按事件:boolean onLongClick(View v);,你的函数可以定义成这样:boolean onMyLongClick(View v);

      看到了么,函数名不重要,重要的是函数签名,Signture!!!

      好吧,说简单点,就是要注意,函数参数类型、返回值类型要相同,其它随便整。

    2. 这步好简单,只需要在对应控件中进行绑定即可:

      <EditText
          android:onClick="@{employ::onEmployClick}"/>
      

      employ是在标签中申明的变量,通过该变量,将刚才定义的点击事件监听函数绑定在一起。

      两个冒号**:*,和C++中的用法类似,一种域名的概念。当然你也可以用点"."的方式绑定,就像这样:

      <EditText
      	android:onClick="@{employ.onEmployClick}"/>
      
    3. 第三步直接将ViewModel和XML的View绑定在一起就好了:

      代码大概是这样:

      EditBinding binding = DataBindingUtil.setContentView(this, R.layout.edit_main);
      binding.setEmploy(employ);
      

    就是这么简单,来看看完整代码:

    布局文件:edit_main.xml:

    <?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">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <EditText
                android:id="@+id/et_employ"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:onClick="@{employ::onEmployClick}"
                android:text="@={employ.mName, default=`Stephen`}" />
    
        </LinearLayout>
    
        <data class="EditBinding">
    
            <variable
                name="employ"
                type="com.superli.mvvmdemo.Employ" />
        </data>
    </layout>
    

    Model类 :Employ.java:

    public class Employ {
        private static final String TAG = "Employ";
    
        public final ObservableField<String> mName = new ObservableField<>();
    
        public Employ(String name) {
            mName.set(name);
        }
    
        public void onEmployClick(View v) {
            Log.i(TAG, "onClick");
        }
    }
    

    Activity :EditTextActivity.java

    public class EditTextActivity extends Activity {
        private Employ employ = new Employ("Kevin");
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            EditBinding binding = DataBindingUtil.setContentView(this, R.layout.edit_main);
            binding.setEmploy(employ);
        }
    }
    

    监听绑定

    签名绑定的方式,要接事件的接收函数签名,必须与对应的函数签名一致。而监听绑定则打破了这种限制,它只需要让函数返回值一致,函数参数则可以自定义。

    绑定签名中的代码,修改成监听绑定的代码,是这样的:

    Model类:Employ.java

    public class Employ {
        private static final String TAG = "Employ";
    
        public final ObservableField<String> mName = new ObservableField<>();
    
        public Employ(String name) {
            mName.set(name);
        }
    
        public void onEmployClick(View v, String str) { // 不再局限于函数签名对参数个数限制
            Log.i(TAG, "onClick, str = " + str);
        }
    }
    

    布局文件:edit_main.xml:

    
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <EditText
                android:id="@+id/et_employ"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:onClick="@{(view) -> employ.onEmployClick(view, "SuperLi")}"
                android:text="@={employ.mName, default=`Stephen`}" />
    
        LinearLayout>
    
        <data class="EditBinding">
    
            <variable
                name="employ"
                type="com.superli.mvvmdemo.Employ" />
        data>
    layout>
    

    稍微解释一下:

    android:onClick="@{(view) -> employ.onEmployClick(view, "SuperLi")}"
    

    通过lambda表达式,将点击事件发生的View传递给绑定的函数。

    • ":为xml语言中英文引号的转义符。

    你可能感兴趣的:(设计模式)