天天看點

學習筆記:DataBinding(二): 資料綁定

本節學習使用可觀察的資料對象進行單向和雙向資料綁定

使用DataBinding,我們可以使用之前我們已知的原始的基本類型、引用類型資料,但這些資料的改變不會使UI自動更新。

DataBinding為我們提供了資料驅動視圖的可觀察資料對象: objects(對象), fields(字段), collections(集合)。使用它們綁定UI,當這些對象的屬性發生改變,UI會自動更新。

Observable fields

DataBinding為我們包裝好的基本類型的可觀察字段:

  • ObservableBoolean

  • ObservableByte

  • ObservableChar

  • ObservableShort

  • ObservableInt

  • ObservableLong

  • ObservableFloat

  • ObservableDouble

  • ObservableParcelable

我們自己也可通過ObservableField 泛型來申明其他類型:

class User {
	// 普通類型
    var firstName: String? = null 
    // 使用ObservableField自己封裝
    var lastName: ObservableField<String>? = null
    // 使用官方提供已包裝的
    var age: ObservableInt? = null
}
           
class MyHandler(private val mContext: Context) {
    // 伴生對象,相當于Java中的靜态成員
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(user: User) {
        Toast.makeText(mContext, "點選了:" + user.firstName, Toast.LENGTH_SHORT).show()

        user.firstName = "你好"
        // 注意調用,不是= ,而是相當于Java中斷getLastName().set(String)
        // 直接=,相當于setLastName(),不會自動重新整理的
        user.lastName!!.set("DataBinding")
        // !!非空斷言運算符将任何值轉換為非空類型,若該值為空則抛出異常,因為user.age是可空對象,是以需要使用
        user.age!!.set(2)
    }

}
           
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="com.example.databindingsample.User" />

        <variable
            name="user"
            type="User" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{user.lastName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{String.valueOf(user.age)}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(user)}"
            android:text="屬性更改" />

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        val user = User()
        user.firstName = "helllo"
        user.lastName = ObservableField("databinding")
        user.age = ObservableInt(1)
        binding!!.user = user

        binding!!.myHandler = MyHandler(this)
    }
}
           

效果圖:

學習筆記:DataBinding(二): 資料綁定

可以看到,我們使用ObservableField和ObservableInt的字段屬性值改變時,視圖也随之改變了,而原始的資料類型是屬性值改變是不會引起視圖更新的。

Observable collections

  • ObservableArrayMap

  • ObservableArrayList

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="com.example.databindingsample.User" />

        <variable
            name="list"
            type="androidx.databinding.ObservableList&lt;String>" />

        <variable
            name="map"
            type="androidx.databinding.ObservableMap&lt;String,String&gt;" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{map[`first`]}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text='@{map["second"]}' />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{list[0]}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(map,list)}"
            android:text="屬性更改" />

    </LinearLayout>

</layout>
           
class MyHandler(private val mContext: Context) {
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(map: ObservableMap<String,String>,list: ObservableList<String>) {
        Toast.makeText(mContext,"點選了",Toast.LENGTH_SHORT).show()

        map.put("first","android")
        map.put("second","iOS")

        list[0] = "GO"
    }

}
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        val observableMap = ObservableArrayMap<String, String>().apply {
            put("first","kotlin")
            put("second","java")
        }

        val observableList = ObservableArrayList<String>().apply {
            add("kotlin")
            add("java")
        }
        
        binding!!.map = observableMap
        binding!!.list = observableList

        binding!!.myHandler = MyHandler(this)
    }
}
           

效果如下:

學習筆記:DataBinding(二): 資料綁定

Observable objects

DataBinding給我們提供了Observable,讓類繼承Observable後,當類對象屬性變化時通過

notifyPropertyChanged

notifyChange

來通知UI重新整理。

class User : BaseObservable() {

    // 将@Bindable注解用于屬性getter
    // @Bindable注解應該應用于一個可觀察類的任何getter通路方法。Bindable将在BR類中生成一個字段來辨別該字段,用于當該屬性發生改變時
    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            // 隻更新本字段
//            notifyPropertyChanged(BR.firstName)

            // 更新所有字段
            notifyChange()
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
//            notifyPropertyChanged(BR.lastName)
        }

    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
//            notifyPropertyChanged(BR.age)
        }
}
           
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="com.example.databindingsample.User" />

        <variable
            name="user"
            type="User" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{user.lastName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{String.valueOf(user.age)}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(user)}"
            android:text="屬性更改" />

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        binding!!.user = User().apply {
            firstName = "1"
            lastName = "2"
            age = 3
        }

        binding!!.myHandler = MyHandler(this)
    }
}
           
class MyHandler(private val mContext: Context) {
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(user: User) {
        Toast.makeText(mContext,"點選了",Toast.LENGTH_SHORT).show()

        user.apply {
            firstName = "4"
            lastName = "5"
            age = 6
        }

    }
}
           

效果:

學習筆記:DataBinding(二): 資料綁定

注意:

  • 在Kotlin中使用注解,需要在app build.gradle中添加Kotlin注解插件:

    apply plugin: 'kotlin-kapt'

    , 否則會編譯失敗:compileDebugKotlin FAILED
  • BR類似于R檔案,DataBinding自動生成的一個資源ID檔案;另外這個id值并不固定,每次build可能都不一樣

Two-way data binding

以上我們學習的DataBinding使用,都是使用的單向綁定,也就是資料驅動視圖,下面我們來學習,資料與視圖雙向驅動,資料改變可以使UI重新整理,UI改變也可以讓資料更新,也就是DataBinding的雙向資料綁定。

先看下面這樣一個小栗子:

Model類和UI中的Checkbox,我們希望Checkbox的選中狀态可以根據Model的isChecked屬性重新整理,同時Checkbox選中狀态主動改變時,也更新Model的isChecked屬性

學習筆記:DataBinding(二): 資料綁定

我嘗試用之前單向綁定的方式去做了一下,如下代碼:

class Model : BaseObservable(){

    @get:Bindable
    var isChecked: Boolean = false
        set(value) {
            field = value
            notifyChange()
        }
}
           
class MyHandler(private val mContext: Context) {

    fun onCheckedChanged(isChecked: Boolean,model: Model){

        if(model.isChecked != isChecked){
            model.isChecked = isChecked
            Toast.makeText(mContext,"視圖驅動資料:" + model.isChecked,Toast.LENGTH_SHORT).show()
        }
    }

    fun onClick(model: Model){
        model.isChecked = !model.isChecked

        Toast.makeText(mContext,"資料驅動視圖:" + model.isChecked,Toast.LENGTH_SHORT).show()
    }

}
           
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data class="MainActivityBinding">

        <variable
            name="model"
            type="com.example.databindingsample.Model" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <CheckBox
            android:id="@+id/cb"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="@{model.isChecked}"
            android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="Model更改屬性值"
            android:onClick="@{() -> myHandler.onClick(model)}"/>

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: MainActivityBinding? = null

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

        binding!!.model = Model()

        binding!!.myHandler = MyHandler(this)

    }
}
           

之是以采用資料類繼承BaseObservable方式實作,是因為我在嘗試使用ObservableBoolean如下方式時,卻出現了讓我費解的情況,代碼如下:

class Model{
    var isChecked: ObservableBoolean = ObservableBoolean(false)
}
           
class MyHandler(private val mContext: Context) {

    fun onCheckedChanged(isChecked: Boolean,model: Model){

        if(model.isChecked.get() != isChecked){
            // 關鍵問題在這行代碼
            model.isChecked = ObservableBoolean(isChecked)
            Toast.makeText(mContext,"視圖驅動資料:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
        }
    }

    fun onClick(model: Model){
        model.isChecked.set(!model.isChecked.get())

        Toast.makeText(mContext,"資料驅動視圖:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

}
           

就這樣其他代碼不變,我的本意是在視圖驅動視圖時,更新Model的isChecked屬性通過model.isChecked = ObservableBoolean(isChecked) ,使用 = 直接指派,不調用set()重新整理了,就出現了如下的情況,單獨點選CheckBox狀态更新沒有問題,單獨點選Button更新CheckBox也沒問題,但是隻要點選了CheckBox再去點選Button就無法再重新整理視圖了,如下效果圖:

學習筆記:DataBinding(二): 資料綁定

model.isChecked = ObservableBoolean(isChecked) 替換成 model.isChecked.set(isChecked) 就正常了

這個問題我搞不懂,知道原因的還請為我解惑,先行謝過了!

進入正題,下面來看這個讓我浪費很長時間的問題,DataBinding怎麼使用雙向綁定來搞定的,更改代碼如下:

<CheckBox
    android:id="@+id/cb"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:checked="@={model.isChecked}" 
    android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>
           
class MyHandler(private val mContext: Context) {

    // 這個監聽隻為輸出屬性值
    fun onCheckedChanged(model: Model){
        Toast.makeText(mContext,"視圖驅動資料:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

    fun onClick(model: Model){
        model.isChecked.set(!model.isChecked.get())

        Toast.makeText(mContext,"資料驅動視圖:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

}
           

效果如下:

學習筆記:DataBinding(二): 資料綁定

如上代碼MainActivity代碼不變,使用

@={}

符号即可完成雙向綁定,就代替了我之前的onCheckedChanged監聽,和其中的一些處理,如果不是為了Toast屬性值,xml中的onCheckedChanged是不用設定的

細心的話會發現,點選CheckBox時輸出的Model.isChecked是反的,其實沒問題,是因為CheckBox的onCheckedChanged回調在Model.isChecked屬性改變之前,拿到的還是之前的值

繼續閱讀