MVVM&Android實踐(二):動態綁定

第二部分:動態綁定

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文件:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <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文件和前面的也沒什麼不同:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="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<T>

對於該類,搜索了一下各類技術博客,發現大家都在使用一套讓人一頭霧水的代碼。本人行文儘量不要多餘代碼,讓初通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<? extends T> collection) 將集合數據整體添加進對象
boolean addAll (int index, Collection<? extends T> collection) 在指定位置插入集合
void clear() 清除所有數據
T remove(int index) 刪除指定index的數據

當然,不止這些,想要了解更多,請移步

官網

或者自行查看源碼。

在使用ObserableArrayList進行數據的增刪改查時,請注意你要操作的當前Index是否真是存在,如果不存在將會發生運行時異常。Index隨着數據的添加(調用add(T object))函數而自增,所以,在操作Index前,請判斷該Index是否存在。

在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"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <!-- ObservableArrayList類型需要額外導包 -->
        <import type="androidx.databinding.ObservableArrayList"/>
        <variable
            name="observableUser"
            type="ObservableArrayList&lt;Object&gt;" />
    </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. <import>標籤導入ObservableArrayList包:前面在使用String時,我們並沒有用到<import>標籤,是因爲String類型默認已經導入。但ObservableArrayList並沒有包含在默認導入的列表中,我們需要手動導入。

  2. 聲明ObservableArrayList變量名爲observableUsertype="ObservableArrayList&lt;Object&gt;"這個地方有兩個在Android中不常用的符號&lt;&gt;,它們分別時xml中間中的<>符號,替換一下就是:type="ObservableArrayList<Object>",是不是明白了?

    因爲<>兩個符號被xml用作標籤的語法符號,所以其它需要的地方就只能用對應的轉移字符了。記住:&lt;&gt;分別時<>的轉義字符。

  3. android:text="@{observableUser[1]}":這行代碼表示了ObservableArrayList類型對象的典型使用方式,通過index取值,如果你知道它底層數據結構是數組,就能理解,這完全就是數組的取值方式。

還算是挺簡單的吧。

ObserableArrayMap<K,V>

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中的使用
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <import type="androidx.databinding.ObservableArrayMap"/>
        <variable
            name="mapUser"
            type="ObservableArrayMap&lt;String, String&gt;" />
    </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:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <!-- 還記得嗎?class的值表示自定義binding類名,忘了往前翻翻 -->
    <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:

<?xml version="1.0" encoding="utf-8"?>
<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:

<?xml version="1.0" encoding="utf-8"?>
<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的方式。

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

        <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的方式。

<?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="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的類型改爲可觀察的數據類型。

<?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="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&lt;String&gt;" />
    </data>
</layout>

Adataper&ViewHolder:NumerAdapter.java:

主要將mData的數據類型,從ArrayList<String>改爲ArrayList<ObservableField<String>>

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是在<data>標籤中申明的變量,通過該變量,將剛纔定義的點擊事件監聽函數綁定在一起。

    兩個冒號**:😗*,和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:

<?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="@{(view) -> employ.onEmployClick(view, &quot;SuperLi&quot;)}"
            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, &quot;SuperLi&quot;)}"

通過lambda表達式,將點擊事件發生的View傳遞給綁定的函數。

  • &quot;:爲xml語言中英文引號的轉義符。
發佈了79 篇原創文章 · 獲贊 30 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章