本篇文章以這個示例BasicSample來研究學習Android DataBinding的基本用法株汉。
開始使用
首先請在應(yīng)用模塊的 build.gradle 文件中添加 dataBinding 元素筐乳,如下所示
android {
...
dataBinding {
enabled = true
}
}
然后將常規(guī)的布局文件轉(zhuǎn)化為數(shù)據(jù)綁定布局文件
<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>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- 使用
<layout>
標簽包裹常規(guī)的布局文件 - 添加
<data>
標簽,在<data>
標簽內(nèi)添加布局變量和布局表達式(不是必須的)
可以使用Android Studio的快捷功能將普通布局轉(zhuǎn)化為數(shù)據(jù)綁定布局
將鼠標懸停在布局文件的根元素上乔妈,點擊左側(cè)出現(xiàn)的黃色小燈哥童,然后選擇Convert to Data binding layout
。
BasicSample如下所示
本篇文章主要學習一下幾點
- 布局變量和布局表達式
- 觀察能力褒翰,通過可觀察的變量贮懈,LiveData 和可觀察的類實現(xiàn)觀察能力
- 綁定適配器,綁定方法和綁定轉(zhuǎn)換器
- 和ViewModels無縫結(jié)合
布局變量和布局表達式
使用布局變量和布局表達式可以少寫很多樣板代碼和重復(fù)代碼优训。布局變量和布局表達式將一些UI操作從activities and fragments移到XML布局文件中朵你。
例如,我們要給一個TextView動態(tài)設(shè)置text揣非,我們會在activity中這樣寫
TextView textView = findViewById(R.id.name);
textView.setText(user.name);
但是使用數(shù)據(jù)綁定抡医,我們可以直接在XML布局文件中直接將布局變量
賦值給TextView的text
屬性
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}" />
具體實現(xiàn)
首先定義我們用到的數(shù)據(jù)類
data class ObservableFieldProfile(
val name: String,
val lastName: String,
val likes: ObservableInt
)
修改布局文件
<?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>
<!--注釋1處-->
<variable
name="user"
type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.databinding.basicsample.ui.BlogDemoActivity">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
在注釋1處,我們在data
標簽內(nèi)定義了user變量早敬,類型是ObservableFieldProfile忌傻,然后我們給TextView的屬性賦值為user.name
android:text="@{user.name}"
修改布局文件以后,我們點擊一下Make project
搞监,數(shù)據(jù)綁定框架會為我們生成一些用用的類水孩。
然后我們還要修改Activity,如下所示
class BlogDemoActivity : AppCompatActivity() {
//定義我們的數(shù)據(jù)
private val fieldProfile = ObservableFieldProfile("Ada", "Lovelace",
ObservableInt(0))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//注釋1處琐驴,將Activity和布局文件綁定
val binding: ActivityBlogDemoBinding = DataBindingUtil
.setContentView(this, R.layout.activity_blog_demo)
//注釋2處俘种,為布局變量user賦值
binding.user = fieldProfile
}
}
這個ActivityBlogDemoBinding
類,就是數(shù)據(jù)綁定框架幫我們生成的中間類绝淡,我們看見生成的類名和我們的布局文件名字是對應(yīng)的宙刘。布局文件名是activity_blog_demo
,生成的綁定類名是ActivityBlogDemoBinding
牢酵⌒可以看到就是把布局文件名轉(zhuǎn)成駝峰命名然后在后面加上Binding。
我們也可以自定義生成的綁定類名馍乙,如下所示:
<data class="ActivityMyBlogDemoBinding">
</data>
我們運行一下可以看到TextView的text
顯示是Ada
布近。
觀察能力
當數(shù)據(jù)改變的時候為了能自動實現(xiàn)UI更新垫释,需要將可觀察的對象和View的屬性綁定。有三種機制可以實現(xiàn)這個目標:可觀察的變量吊输,LiveData 和可觀察的類饶号。
可觀察的變量(Observable fields)
數(shù)據(jù)綁定框架提供了像ObservableInt铁追,ObservableBoolean來替代原始數(shù)據(jù)類型季蚂,使其具備可觀察能力。提供了ObservableField來替代引用數(shù)據(jù)類型琅束,使其具備可觀察能力扭屁。
我們在上面定義的ObservableFieldProfile類啥寇,它的likes
就是ObservableInt類型的音五。
我們將likes
綁定到TextView的text
屬性,然后手動改變likes
讳推,我們觀察TextView的text
是否也跟著改變艾船。
<TextView
android:id="@+id/tvLikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(user.likes)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnChangeLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change like"
android:textAllCaps="false" />
這里有一點要注意葵腹,
android:text="@{Integer.toString(user.likes)}"
因為likes
就是ObservableInt類型,我們使用的時候會自動轉(zhuǎn)化成int類型屿岂,將likes
賦值給text屬性的時候践宴,需要將likes
轉(zhuǎn)化成字符串類型。這里我們可以看到爷怀,我們可以直接在布局文件中不需要導(dǎo)入就可以使用一些常見的類阻肩,Integer,String运授,等等烤惊,具體哪些類有待完善。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//注釋1處吁朦,將Activity和布局文件綁定
val binding: ActivityBlogDemoBinding = DataBindingUtil
.setContentView(this, R.layout.activity_blog_demo)
binding.btnChangeLike.setOnClickListener {
//增加likes
fieldProfile.likes.set(fieldProfile.likes.get() + 1)
}
}
我們點擊按鈕發(fā)現(xiàn)柒室,likes增加的時候,TextView的text
也會跟著變逗宜。
LiveData
LiveData是Android Architecture Components 中的一個可觀察者具有生命周期感知能力伦泥。和可觀察的變量相比,LiveData的優(yōu)勢是支持 Transformations并且能和其他組件和庫一起配合使用锦溪,例如Room和WorkManager不脯。
我們定義一個類ProfileLiveDataViewModel
。ViewModel是一個用來為Activity或者Fragment準備和管理數(shù)據(jù)的類刻诊。ViewModel可以通過LiveData來提供數(shù)據(jù)防楷。關(guān)于ViewModel和LiveData可以參考ViewModel和LiveData 使用。
class ProfileLiveDataViewModel : ViewModel() {
private val _likes = MutableLiveData(0)
val likes: LiveData<Int> = _likes //暴露一個不可更改的LiveData
//改變_likes
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
}
看看布局文件的改變则涯,只保留了關(guān)鍵信息
<data>
<!--注釋1處-->
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.databinding.basicsample.BlogDemo2Activity">
<TextView
android:id="@+id/tvLikes"
android:text="@{Integer.toString(viewmodel.likes)}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
聲明了布局變量viewmodel复局,類型是ProfileLiveDataViewModel冲簿。將viewmodel的likes賦值給TextView的text
屬性。
Activity的改變
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityBlogDemo2Binding>(this, R.layout.activity_blog_demo2)
//獲取viewModel對象
val viewModel = ViewModelProvider(this).get(ProfileLiveDataViewModel::class.java)
// 賦值給布局變量
binding.viewmodel = viewModel
//注釋1處亿昏,
//給binding設(shè)置生命周期持有者
//LiveData需要生命周期持有者峦剔,因為LiveData數(shù)據(jù)改變的時候只會通知生命周期處于活動狀態(tài)的觀察者
binding.lifecycleOwner = this
btnChangeLike.setOnClickListener {
viewModel.onLike()
}
}
//...
獲取viewmodel賦值給布局變量,注釋1處角钩,我們要給binding設(shè)置生命周期持有者吝沫。LiveData需要生命周期持有者,因為LiveData數(shù)據(jù)改變的時候只會通知生命周期處于活動狀態(tài)的觀察者递礼。
點擊btnChangeLike也是可以改變TextView的text惨险。
可觀察的類(Observable classes)
為了更加靈活可控,你可以實現(xiàn)一個完整的可觀察的類來決定何時更新哪些變量脊髓。這允許你創(chuàng)建變量之間的依賴關(guān)系并且只更新部分UI辫愉。
/**
* A ViewModel that is also an Observable, to be used with Data Binding.
*/
open class ObservableViewModel : ViewModel(), Observable {
//屬性改變注冊器
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
//添加觀察者
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
//移除觀察者
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* 通知觀察者,當前對象的所有屬性發(fā)生了該變将硝。
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* 通知觀察者指定的屬性發(fā)生了改變恭朗。
* 改變的屬性的getter方法應(yīng)該使用Bindable來注解,用于在`BR`類中生成一個field依疼。
*
* @param fieldId 為可綁定的屬性在BR類中生成的field
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
我們自定義了一個ObservableViewModel類并且繼承了Observable痰腮。所以O(shè)bservableViewModel是一個可觀察者。我們還定義了添加涛贯、移除和通知觀察者的方法诽嘉。
要注意的是:改變的屬性的getter方法應(yīng)該使用Bindable來注解,用于在BR
類中生成一個field
弟翘,如下所示虫腋。
//繼承ObservableViewModel類
class ProfileObservableViewModel : ObservableViewModel() {
val likes = ObservableInt(0)
fun onLike() {
likes.increment()
notifyPropertyChanged(BR.popularity)
}
//注釋1處
@Bindable
fun getPopularity(): Popularity {
return likes.get().let {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
}
}
//流行度
enum class Popularity {
NORMAL,//正常
POPULAR,//流行
STAR//明星
}
//定義ObservableInt的擴展函數(shù)
private fun ObservableInt.increment() {
set(get() + 1)
}
在注釋1處,使用Bindable注解getPopularity方法稀余。
@Bindable
fun getPopularity(): Popularity {
}
點擊Make project
悦冀,可以在生成的BR類中看到popularity的field
。
public class BR {
//...
public static final int popularity = 2;
}
在上面的例子中睛琳,當onLike
方法被調(diào)用的時候盒蟆,likes
會增加并且popularity
屬性也會收到數(shù)據(jù)改變的通知(popularity的值依賴likes)。getPopularity
方法會被數(shù)據(jù)綁定框架調(diào)用师骗,返回一個可能發(fā)生了變化的新值历等。
Bindable
注解應(yīng)該被應(yīng)用到Observable
類中的所有g(shù)etter方法上。Bindable會在BR類中生成一個field
來標識Observable
類中發(fā)生改變的屬性辟癌。
綁定適配器寒屯,綁定方法和綁定轉(zhuǎn)換器
綁定適配器(Binding adapters)
綁定適配器可以用來自定義布局屬性。例如你可以為ProgressBar定義app:progressTint
屬性,根據(jù)外部的值來改變進度條的顏色寡夹。
@BindingAdapter("app:progressTint")
@JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {
val color = getAssociatedColor(popularity, view.context)
view.progressTintList = ColorStateList.valueOf(color)
}
//根據(jù)流行度返回不同的顏色
private fun getAssociatedColor(popularity: Popularity, context: Context): Int {
return when (popularity) {
Popularity.NORMAL -> context.theme.obtainStyledAttributes(
intArrayOf(android.R.attr.colorForeground)).getColor(0, 0x000000)
Popularity.POPULAR -> ContextCompat.getColor(context, R.color.popular)
Popularity.STAR -> ContextCompat.getColor(context, R.color.star)
}
}
在布局文件中使用
<ProgressBar
app:progressTint="@{viewmodel.popularity}" />
使用綁定適配器可以將在activity中的UI調(diào)用移到靜態(tài)方法中处面,利于封裝。
你也可以在綁定適配器中使用多個屬性菩掏。
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
@JvmStatic
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
progressBar.progress = (likes * max / 5).coerceAtMost(max)
}
在這個綁定適配器方法中魂角,要求傳入兩個參數(shù),likes
智绸,max
野揪,對應(yīng)布局文件中的"app:progressScaled"
屬性和"android:max"
屬性。
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:max="@{100}"
app:hideIfZero="@{viewmodel.likes}"
app:progressScaled="@{viewmodel.likes}"
app:progressTint="@{viewmodel.popularity}"
tools:progressBackgroundTint="@android:color/darker_gray" />
綁定方法和綁定轉(zhuǎn)換器(Binding methods and binding converters)
當綁定適配器方法很簡單的時候传于,你可以使用綁定方法和綁定轉(zhuǎn)換器來減少代碼囱挑。你可以在 official guide中查閱細節(jié)醉顽。
例如沼溜,當一個屬性值需要傳遞給一個適配器方法:
@BindingAdapter("app:srcCompat")
@JvmStatic fun srcCompat(view: ImageView, @DrawableRes drawableId: Int) {
view.setImageResource(drawable)
}
上面的適配器方法可以使用綁定方法替代。綁定方法可以被添加到工程中的任意的類中游添。
@BindingMethods(
BindingMethod(type = ImageView::class,
attribute = "app:srcCompat",
method = "setImageResource"))
class MyBindingMethods
我們將這個綁定方法添加到MyBindingMethods上系草。BindingMethod注解的三個屬性我們也留意一下:
- type = ImageView::class,屬性關(guān)聯(lián)的View類
- attribute = "app:srcCompat"唆涝,屬性名稱找都,所有的android屬性使用
android:
命名空間,應(yīng)用自定義屬性不用使用命名空間廊酣。 - method = "setImageResource"能耻,被數(shù)據(jù)綁定框架調(diào)用,用來設(shè)置屬性值的方法亡驰。
綁定轉(zhuǎn)換器(使用的時候要小心)
在這個例子中晓猛,我們展示了一個View依賴一個數(shù)字是否為0來決定View是否可見。有很多方法來實現(xiàn)這個功能凡辱。本例會展示兩種實現(xiàn)方式戒职。
我們的目標是根據(jù)likes
的值來控制View的顯示或者隱藏。
android:visibility="@{viewmodel.likes}"
這種方式是不能正常工作的透乾。likes
是一個整數(shù)洪燥,View的visibility
屬性也是一個整數(shù) (VISIBLE, GONE and INVISIBLE are 0, 4 and 8 respectively)。所以直接這樣使用的話乳乌, 可以編譯運行捧韵,但是結(jié)果肯定不是我們想要的。
一個可能的解決方式如下:
android:visibility="@{viewmodel.likes == 0 ? View.GONE : View.VISIBLE}"
布局表達式汉操,增加復(fù)雜度不利于閱讀和維護再来,不推薦使用。
第一種實現(xiàn)方式客情,就是使用綁定轉(zhuǎn)換器(不推薦的方法)
object ConverterUtil {
@JvmStatic fun isZero(number: Int): Boolean {
return number == 0
}
}
我們首先在ConverterUtil類中定義一個isZero
方法其弊,然后在布局文件中導(dǎo)入ConverterUtil類
<data>
<import type="com.example.android.databinding.basicsample.util.ConverterUtil" />
...
</data>
//使用
android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"
這樣就行了癞己?顯然不可以,ConverterUtil的isZero方法返回值是一個boolean類型梭伐。而android:visibility接收一個Integer類型痹雅。所以我們還需要定義一個綁定轉(zhuǎn)換器方法,將這個boolean類型值轉(zhuǎn)化為Integer類型糊识。如下所示:
object BindingConverters{
//BindingConversion使用注解
@BindingConversion
@JvmStatic fun booleanToVisibility(isNotVisible: Boolean): Int {
return if (isNotVisible) View.GONE else View.VISIBLE
}
}
android:visibility
接收了一個boolean類型參數(shù)以后绩社,就會去找是否有可以把boolean類型值轉(zhuǎn)化為Integer類型的綁定方法,如果找到了赂苗,就會進行轉(zhuǎn)換愉耙。在編譯期間,編譯器會為我們做檢查拌滋,如果找不到這樣的方法就會報錯朴沿,編譯不過。
注意:這種轉(zhuǎn)換是不安全的败砂,因為我們無法將其限制在我們這一種場景下赌渣。如果一個View屬性接收Integer類型,然后我們給該View傳遞了一個boolean類型的值昌犹,那么它會將該boolean值轉(zhuǎn)化為View可見性的Integer值坚芜。在這個例子中就是View.GONE(8)或者View.VISIBLE(0)。
推薦的方法斜姥,使用綁定適配器
@BindingAdapter("app:hideIfZero") // Recommended solution
@JvmStatic fun hideIfZero(view: View, number: Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
app:hideIfZero="@{viewmodel.likes}"
這種通過自定義一個新的屬性的方式鸿竖,可以避免被意外使用。
根據(jù)經(jīng)驗铸敏,使用綁定適配器創(chuàng)建自己的自定義屬性比在布局表達式中增加邏輯更好缚忧,也比綁定轉(zhuǎn)換器更安全,推薦使用搞坝。
和ViewModels無縫結(jié)合
上面在說LiveData的時候提到了搔谴,這里就不說了。
完整代碼請參考官方示例BasicSample