Android DataBinding 基本用法

本篇文章以这个示例BasicSample来研究学习Android DataBinding的基本用法。

开始使用

首先请在应用模块的 build.gradle 文件中添加 dataBinding 元素,如下所示

android {
        ...
        dataBinding {
            enabled = true
        }
    }

然后将常规的布局文件转化为数据绑定布局文件

<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>
  1. 使用<layout>标签包裹常规的布局文件
  2. 添加<data>标签,在<data>标签内添加布局变量和布局表达式(不是必须的)

可以使用Android Studio的快捷功能将普通布局转化为数据绑定布局


convert_layout.png

将鼠标悬停在布局文件的根元素上,点击左侧出现的黄色小灯,然后选择Convert to Data binding layout

BasicSample如下所示

screenshotbasic.png

本篇文章主要学习一下几点

  1. 布局变量和布局表达式
  2. 观察能力,通过可观察的变量,LiveData 和可观察的类实现观察能力
  3. 绑定适配器,绑定方法和绑定转换器
  4. 和ViewModels无缝结合

布局变量和布局表达式

使用布局变量和布局表达式可以少写很多样板代码和重复代码。布局变量和布局表达式将一些UI操作从activities and fragments移到XML布局文件中。

例如,我们要给一个TextView动态设置text,我们会在activity中这样写

TextView textView = findViewById(R.id.name);
textView.setText(user.name);

但是使用数据绑定,我们可以直接在XML布局文件中直接将布局变量赋值给TextView的text属性

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name}" />

具体实现
首先定义我们用到的数据类

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标签内定义了user变量,类型是ObservableFieldProfile,然后我们给TextView的属性赋值为user.name

android:text="@{user.name}"

修改布局文件以后,我们点击一下Make project,数据绑定框架会为我们生成一些用用的类。

然后我们还要修改Activity,如下所示

class BlogDemoActivity : AppCompatActivity() {
    
    //定义我们的数据
    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类,就是数据绑定框架帮我们生成的中间类,我们看见生成的类名和我们的布局文件名字是对应的。布局文件名是activity_blog_demo,生成的绑定类名是ActivityBlogDemoBinding。可以看到就是把布局文件名转成驼峰命名然后在后面加上Binding。

我们也可以自定义生成的绑定类名,如下所示:

    <data class="ActivityMyBlogDemoBinding">

    </data>

我们运行一下可以看到TextView的text显示是Ada

观察能力

当数据改变的时候为了能自动实现UI更新,需要将可观察的对象和View的属性绑定。有三种机制可以实现这个目标:可观察的变量,LiveData 和可观察的类。

可观察的变量(Observable fields)

数据绑定框架提供了像ObservableInt,ObservableBoolean来替代原始数据类型,使其具备可观察能力。提供了ObservableField来替代引用数据类型,使其具备可观察能力。
我们在上面定义的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类型,我们使用的时候会自动转化成int类型,将likes赋值给text属性的时候,需要将likes转化成字符串类型。这里我们可以看到,我们可以直接在布局文件中不需要导入就可以使用一些常见的类,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)
        }
    }

我们点击按钮发现,likes增加的时候,TextView的text也会跟着变。

LiveData

LiveData是Android Architecture Components 中的一个可观察者具有生命周期感知能力。和可观察的变量相比,LiveData的优势是支持 Transformations并且能和其他组件和库一起配合使用,例如Room和WorkManager。

我们定义一个类ProfileLiveDataViewModel。ViewModel是一个用来为Activity或者Fragment准备和管理数据的类。ViewModel可以通过LiveData来提供数据。关于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
    }
}

看看布局文件的改变,只保留了关键信息

    <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设置生命周期持有者
        //LiveData需要生命周期持有者,因为LiveData数据改变的时候只会通知生命周期处于活动状态的观察者
        binding.lifecycleOwner = this

        btnChangeLike.setOnClickListener {
            viewModel.onLike()
        }
    }
//...

获取viewmodel赋值给布局变量,注释1处,我们要给binding设置生命周期持有者。LiveData需要生命周期持有者,因为LiveData数据改变的时候只会通知生命周期处于活动状态的观察者。

点击btnChangeLike也是可以改变TextView的text。

可观察的类(Observable classes)

为了更加灵活可控,你可以实现一个完整的可观察的类来决定何时更新哪些变量。这允许你创建变量之间的依赖关系并且只更新部分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)
    }

    /**
     * 通知观察者,当前对象的所有属性发生了该变。
     */
    fun notifyChange() {
        callbacks.notifyCallbacks(this, 0, null)
    }

    /**
     * 通知观察者指定的属性发生了改变。
     * 改变的属性的getter方法应该使用Bindable来注解,用于在`BR`类中生成一个field。
     *
     * @param fieldId 为可绑定的属性在BR类中生成的field
     */
    fun notifyPropertyChanged(fieldId: Int) {
        callbacks.notifyCallbacks(this, fieldId, null)
    }
}

我们自定义了一个ObservableViewModel类并且继承了Observable。所以ObservableViewModel是一个可观察者。我们还定义了添加、移除和通知观察者的方法。

要注意的是:改变的属性的getter方法应该使用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的扩展函数
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方法被调用的时候,likes会增加并且popularity属性也会收到数据改变的通知(popularity的值依赖likes)。getPopularity方法会被数据绑定框架调用,返回一个可能发生了变化的新值。

Bindable注解应该被应用到Observable类中的所有getter方法上。Bindable会在BR类中生成一个field来标识Observable类中发生改变的属性。

绑定适配器,绑定方法和绑定转换器

绑定适配器(Binding adapters)

绑定适配器可以用来自定义布局属性。例如你可以为ProgressBar定义app:progressTint属性,根据外部的值来改变进度条的颜色。

@BindingAdapter("app:progressTint")
@JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {

    val color = getAssociatedColor(popularity, view.context)
    view.progressTintList = ColorStateList.valueOf(color)
}

//根据流行度返回不同的颜色
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调用移到静态方法中,利于封装。

你也可以在绑定适配器中使用多个属性。

@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
@JvmStatic
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
    progressBar.progress = (likes * max / 5).coerceAtMost(max)
}

在这个绑定适配器方法中,要求传入两个参数,likesmax,对应布局文件中的"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" />

绑定方法和绑定转换器(Binding methods and binding converters)

当绑定适配器方法很简单的时候,你可以使用绑定方法和绑定转换器来减少代码。你可以在 official guide中查阅细节。

例如,当一个属性值需要传递给一个适配器方法:

@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,属性关联的View类
  • attribute = "app:srcCompat",属性名称,所有的android属性使用android:命名空间,应用自定义属性不用使用命名空间。
  • method = "setImageResource",被数据绑定框架调用,用来设置属性值的方法。

绑定转换器(使用的时候要小心)

在这个例子中,我们展示了一个View依赖一个数字是否为0来决定View是否可见。有很多方法来实现这个功能。本例会展示两种实现方式。

我们的目标是根据likes的值来控制View的显示或者隐藏。

android:visibility="@{viewmodel.likes}"

这种方式是不能正常工作的。likes是一个整数,View的visibility属性也是一个整数 (VISIBLE, GONE and INVISIBLE are 0, 4 and 8 respectively)。所以直接这样使用的话, 可以编译运行,但是结果肯定不是我们想要的。

一个可能的解决方式如下:

android:visibility="@{viewmodel.likes == 0 ? View.GONE : View.VISIBLE}"

布局表达式,增加复杂度不利于阅读和维护,不推荐使用

第一种实现方式,就是使用绑定转换器(不推荐的方法)

object ConverterUtil {
    @JvmStatic fun isZero(number: Int): Boolean {
        return number == 0
    }
}

我们首先在ConverterUtil类中定义一个isZero方法,然后在布局文件中导入ConverterUtil类

<data>
        <import type="com.example.android.databinding.basicsample.util.ConverterUtil" />
        ...
</data>

//使用
android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"

这样就行了?显然不可以,ConverterUtil的isZero方法返回值是一个boolean类型。而android:visibility接收一个Integer类型。所以我们还需要定义一个绑定转换器方法,将这个boolean类型值转化为Integer类型。如下所示:

object BindingConverters{

    //BindingConversion使用注解
    @BindingConversion
    @JvmStatic fun booleanToVisibility(isNotVisible: Boolean): Int {
        return if (isNotVisible) View.GONE else View.VISIBLE
    }
}

android:visibility接收了一个boolean类型参数以后,就会去找是否有可以把boolean类型值转化为Integer类型的绑定方法,如果找到了,就会进行转换。在编译期间,编译器会为我们做检查,如果找不到这样的方法就会报错,编译不过。

注意:这种转换是不安全的,因为我们无法将其限制在我们这一种场景下。如果一个View属性接收Integer类型,然后我们给该View传递了一个boolean类型的值,那么它会将该boolean值转化为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}"

这种通过自定义一个新的属性的方式,可以避免被意外使用。

根据经验,使用绑定适配器创建自己的自定义属性比在布局表达式中增加逻辑更好,也比绑定转换器更安全,推荐使用。

和ViewModels无缝结合

上面在说LiveData的时候提到了,这里就不说了。

完整代码请参考官方示例BasicSample

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容