本節學習使用可觀察的資料對象進行單向和雙向資料綁定
使用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)
}
}
效果圖:
可以看到,我們使用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<String>" />
<variable
name="map"
type="androidx.databinding.ObservableMap<String,String>" />
<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)
}
}
效果如下:
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
}
}
}
效果:
注意:
- 在Kotlin中使用注解,需要在app build.gradle中添加Kotlin注解插件:
, 否則會編譯失敗:compileDebugKotlin FAILED
apply plugin: 'kotlin-kapt'
- BR類似于R檔案,DataBinding自動生成的一個資源ID檔案;另外這個id值并不固定,每次build可能都不一樣
Two-way data binding
以上我們學習的DataBinding使用,都是使用的單向綁定,也就是資料驅動視圖,下面我們來學習,資料與視圖雙向驅動,資料改變可以使UI重新整理,UI改變也可以讓資料更新,也就是DataBinding的雙向資料綁定。
先看下面這樣一個小栗子:
Model類和UI中的Checkbox,我們希望Checkbox的選中狀态可以根據Model的isChecked屬性重新整理,同時Checkbox選中狀态主動改變時,也更新Model的isChecked屬性
我嘗試用之前單向綁定的方式去做了一下,如下代碼:
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就無法再重新整理視圖了,如下效果圖:
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()
}
}
效果如下:
如上代碼MainActivity代碼不變,使用
@={}
符号即可完成雙向綁定,就代替了我之前的onCheckedChanged監聽,和其中的一些處理,如果不是為了Toast屬性值,xml中的onCheckedChanged是不用設定的
細心的話會發現,點選CheckBox時輸出的Model.isChecked是反的,其實沒問題,是因為CheckBox的onCheckedChanged回調在Model.isChecked屬性改變之前,拿到的還是之前的值