Jetpack:Data Binding入门指南

11,002 阅读14分钟

又到周末好时光,开始嗨之前再抽点时间看看本文,能看到最后的都是大佬,收下我的膜拜。本文技术内容讲的是关于Data Binding Library的那点事,有的同学可能解过了,有的娃可能都不知道是什么东东...。为了不落伍,和大家一样优秀,决定写Jetpack方面的文章。与别人不一样的是,会加入自己的理解和栗子,而不是简单的翻译(我英文水平也不行)。如果大家发现有误的地方,希望多加指点,在此谢过。

About Jetpack

一年前有缘看了一下Jetpack,但并没有过多的去关注,最近在看Google IO 2019相关资料,看到了Jetpack的身影,不得不陷入深思,无法自拔。

JetPack的官方说法:

Jetpack 是 Android 软件组件的集合,使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

总结性

  • 加速开发:以组件的形式供我们依赖使用。
  • 消除样板代码:还记得在Activity中一大堆findViewById么?能做的不止这么多。
  • 构建高质量应用:现在化设计、避开bug、向后兼容。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助提高工作效率。可全部使用,也可混合搭配!

以上是对官网的摘录。作为开山之篇,先从架构方向的数据绑定库入门开始,让同学感受它的魅力。

Data Binding Library(数据绑定库)

借助数据绑定库(Data Binding Library),可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。数据绑定库要求在Android 4.0以上,Gradle 1.5.0以上。实践证明Android SDK和Gradle版本越高,对Data Binding的支持越好,越简单,速度越快。

举个栗子,这个栗子不重,两只手指可以举起来:

findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}

栗子中通过findViewById找到TextView组件,并将其绑定到 viewModel 变量的 userName 属性。而下面在布局文件中使用数据绑定库将文本直接分配到TextView组件上,这样就无需调用上述任何 Java 代码。

<TextView  android:text="@{viewmodel.userName}" />

竟然这么好用,为啥不了解看看呢?

配置

在我们的项目build.gradle文件下配置如下代码。

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

如果Gradle插件版本在3.1.0-alpha06以上,可以使用新的Data Binding编译器,有利于加速绑定数据文件的生成。在项目的gradle.properties文件添加如下配置。

android.databinding.enableV2=true

同步一下,没什么问题的话,配置已经成功了~

入门

  • 定义一个数据对象
data class User(var name: String, var age: Int)
  • 布局绑定

我们创建名为activity_main.xml的布局文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.gitcode.jetpack.User"/>
   </data>

   <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">
       //在TextView中使用
       <TextView android:layout_width="match_parent"
                 android:gravity="center"
                 android:text="@{user.name}"
                 android:layout_height="match_parent"/>
   </LinearLayout>
</layout>

布局文件的根元素不再是以往的LinearLayout、RelativeLayout等等,而是layout。在data元素内添加variable,其属性表示声明一个com.gitcode.jetpack.User类型的变量user。如果多个变量的话,可在data元素下添加多个varialbe元素,格式是一致的。

<data>
   <variable name="user" type="com.gitcode.jetpack.User"/>
   <variable name="time" type="com.gitcode.jetpack.Time"/>
</data>

@{}语法中使用表达式将变量赋值给view的属性。例如:这里将user变量的firstName属性赋值给TextView的text属性。

android:text="@{user.firstName}"
  • 绑定数据

此时布局声明的user变量值还是初始值,我们需要为其绑定数据。

默认情况下,会根据目前布局文件名称来生成一个绑定类(binding class),例如当前布局文件名是activity_main,那么生成的类名就是ActivityMainBinding。

绑定类会拥有当前布局声明变量,并声明getter或者setter方法,也就是说ActivityMainBinding类会带有user属性和getUser、setUser方法,变量的默认初始化与Java一致:引用类型为null,int为0,bool为false。

在MainActivity的onCreate()方法中添加如下代码,将数据绑定到布局上。

val binding: ActivityMainBinding 
        = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.user = User("GitCode", 3)

经典代码是这样的:

setContentView(R.layout.activity_main)
val user=User("GitCode",3)
val tvName=findViewById<TextView>(R.id.tvName)
tvName.text = user.name

可有看出,使用数据绑定库会使代码简洁很多,可读性也很高。 运行一下项目,既可以考到效果了~

运行效果

如果是在Fragment、Adapter中使用,那就要换个姿势了。

val listItemBinding = ListItemBinding
            .inflate(layoutInflater, viewGroup, false)
//或者
val listItemBinding = DataBindingUtil
            .inflate(layoutInflater, R.layout.list_item, viewGroup, false)

恭喜,你已经入门了

可以选择继续学习,

看下文

也可以当做了解

点个赞

看看其他文章了~

布局与绑定表达式

在一开始介绍Data Binding Libaray时,就使用了@{}语法,花括号里面的内容称为绑定表达式,绑定表达式其实并不复杂,跟我们正常使用Java和Kotlin语言的表达式没多大区别。那我们可以在表达式中使用什么类型的运算符或者关键字呢?

常用运算符

运算符 符号
算术 加、减、乘、除、求余(+ 、 - 、* 、/、 %)
逻辑 与、或(&&、||)
一元 + 、-、 !、 ~
移位 >>、 >>>、 <<
关系 == 、> 、<、 >= 、<=(使用符号<时,要换成&lt;)

其他常用的

同时也支持字符拼接+,instanceof,分组、属性访问、数组访问、?:、转型、访问调用,基本类型等等等。 也就是说,绑定表达式语言大多数跟宿主代码(Java or Kotlin)的表达式差不多。为什么说是大多数,因为不能使用thissupernewExplicit generic invocation(明确的泛型调用)等。

丢个栗子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

再举丢个栗子:

android:text="@{user.displayName ?? user.lastName}"

如果user.displayName不为null则使用,否则使用user.lastName.在这里也看得出,可以通过表达式访问类的属性。绑定类会自动检查当前变量是否为null,以避免发生空指针异常。栗子:如果user变量为null,那么user.lastName也会是null。

集合

像数组,链表,Maps等常见的集合,都可以采用下标[]访问它们的元素。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
//或者
android:text="@{map.key}"

注意在data元素内添加了import元素,表示导入该类型的定义,这样表达式中引用属性可读性高点,使用也方便。

来个容易掰的栗子:

<data>
    <import type="android.view.View"/>
</data>

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

通过导入View类型,就可以使用相关属性,例如这里的View.VISIBLE

有时导入的类全名太长了或者存在相同类型的类名,我们就可以给它取个别名,然后就可用别名进行coding~

<import type="android.view.View"/>

<import type="com.gitcode.jetpack.View"
        alias="JView"/>

使用资源

使用下面语法:

android:padding="@{@dimen/largePadding}"

相关资源的的表达式引用,贴张官网截图:

事件处理

数据绑定库允许我们在事件到View时候通过表达式去处理它。 在数据绑定库中支持两种机制:方法调用和监听器绑定。

好想一笔带过,因为原文看不明白~~~~(>_<)~~~~
方法调用

点击事件会直接绑定到处理方法上,当一个事件发生,会直接传给绑定的方法。类似我们在布局上使用android:onclick与Activity 的方法绑定。在编译的时候已经绑定,在@{}表达式中的方法如果在Activity找不到或者方法名错误,就会在编译时期报错,方法签名(返回类型和参数相同)一致。

丢个栗子:

定义一个接口,用于处理事件。

//定义一个处理点击事件的类
interface  MethodHandler {
    fun onClick(view: View)
}

在布局声明了methodHandler变量,并在Button的onClick方法使用表达式@{methodHandler::onClick},onClick方法需要与上面接口一致,不然编译器期报错。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        ...
        <variable name="methodHandler"
            type="com.gitcode.jetpack.MethodHandler"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal"
            android:layout_height="match_parent">
         ...
        <Button android:layout_width="wrap_content"
                android:text="Method references"
                android:layout_marginTop="10dp"
                android:onClick="@{methodHandler::onClick}"
                android:layout_height="wrap_content"/>
    </LinearLayout>
</layout>

然后在Activity中实现MethodHandler,并赋值给绑定类的变量。

class MainActivity : AppCompatActivity(), MethodHandler{
    lateinit var binding: ActivityMainBinding

      override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.methodHandler = this
    }
    
    override fun onClick(view: View) {
        Log.i(TAG, "Method handling")
    }
}

因此,当我们点击Button的时候,Activity的onClick方法就会被回调。

监听器绑定

监听器绑定与方法调用不同的是,监听器不再编译器与处理方法绑定,而是在点击事件传递到当前view时,才与处理方法绑定,而且监听器并不要表达式方法名与处理方法同名,只要返回类型一致即可,如果有返回值得话。

来个栗子:

  • 定义接口用于处理事件
interface  ListenerHandler {
    fun onClickListener(view: View)
}
  • 在布局中定义变量和表达式
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="listener" type="com.gitcode.jetpack.ListenerHandler"/>
    </data>

    <Button android:layout_width="wrap_content"
            android:text="Listener"
            android:layout_marginTop="10dp"
            android:onClick="@{(view)->listener.onClickListener(view)}"
            android:layout_height="wrap_content"/>
    </LinearLayout>
<layout>

注意到使用lambda表达式,因此可以在@{}内做更多操作,如预处理数据等。

  • 处理方法 同样在Activity实现ListenerHandler方法,并赋值给绑定类的变量。
class MainActivity : AppCompatActivity(), ListenerHandler {
    lateinit var binding: ActivityMainBinding
    
    override fun onClickListener(view: View) {
        Log.i(TAG, "Listener handling")
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.listener=this
    }
}

点击Button,就能看到onClickListener回调了~

不过瘾的,看官网

好了,讲到这里,大家喝杯奶茶续命,休息会吧~

吃瓜啦

吃完瓜了没?吃完了就该继续撸文了,毕竟革命尚未成功~

绑定类

前面讲的大多数是在布局中去使用表达式,从这开始,讲点代码中的操作。在一开始入门时候,讲到会根据当前布局生成绑定类,绑定类类名由布局名称根据Pascal规则和添加Binding后缀生成。举个栗子就明白了,当前布局名称:activity_shared.xml。生成绑定类名称:ActivitySharedBinding。

那么绑定类的作用是什么?

绑定类是数据绑定库为让我们可以访问布局中的变量和视图而生成的类。

如何创建或者定制绑定类呢?

创建绑定类

  • 使用静态inflate()方法
ActivityMainBinding.inflate(LayoutInflater.from(this))

重载版本

ActivityMainBinding.inflate(getLayoutInflater(), viewGroup, false)
  • 使用静态bind()方法
//一般这种情况是布局有作其他用途
ActivityMainBinding.bind(viewRoot)
  • 在Fragment,ListView,或RecyclerView的adapter使用
val listItemBinding = ListItemBinding.inflate(layoutInflater,
                        viewGroup, false)
// 或者
val listItemBinding = DataBindingUtil
                 .inflate(layoutInflater, R.layout.list_item,
                 viewGroup, false)

定制绑定类

通过修改data元素的class属于达到定制不同名称的绑定类,和其所存储位置。

//生成绑定类名为:ContactItem,存放在当前组件的绑定类包中
<data class="ContactItem">
    …
</data>

//生成绑定类名为:ContactItem,存放在当前组件包中
<data class=".ContactItem">
    …
</data>
//生成绑定类名为:ContactItem,存放在com.gitcode包中
<data class="com.gitcode.ContactItem">
    …
</data>

访问Views

如果需要访问布局中Views,需要给Views添加id,数据绑定库会尽快通过findViewById去绑定。并在Activity中通过绑定类使用。例如:

binding.tvName.text="GitCode"

访问变量

数据绑定库会为在布局中声明的变量在绑定类中生成setter和getter。例如:

binding.user=User("GitCode",3)

绑定类官网

绑定适配器

每个布局表达式都对应着一个绑定适配器,用于进行设置相应属性或监听器所需的框架调用.通俗点说,我们通过调用什么方法去给属性赋值?我们在代码通过setText()方法给view的text属性赋值。讲的就是下面的代码:

binding.tvAge.text="20" //通过tvAge的setText()给TextView的android:text属性赋值

好像跟我们平常调用的没什么区别:

tvAge.text="20"

这里讲的就是这个,当数据变化时,我们调用合适的方法(例如setText方法),去给view的属性赋值(例如android:text的text属性)。还不懂的话,继续看~

给View的属性赋值

数据绑定库提供三种方式让我们去给View的属性赋值:库自己决定选择调用方法;明确指定调用方法;自定义调用逻辑方法。

库自动选择

假如View有个属性color,库会尝试去查找setColor(args)方法,参数args的类型需要和表达式的返回类型一致。例如android:color=@{"black"},因为"black"是字符串类型,所以args的参数类型就是String。命名空间android并没有作强制要求,也可以是gitcode:color=@{"black"}。库查找方法的标准是setXXX()方法名和参数类型,这里的XXX是指属性名。

明确指定

虽然库自动选择已经很智能了,但有时view的属性和方法名并不一致,这是就需要我们明确指定,避免库自动选择找不到。例如ImageView的android:tint属性是关联到setImageTintList(ColorStateList)方法,而不是setTint(),这时,就需要明确指定了。

@BindingMethods(value = [
BindingMethod(
    type = android.widget.ImageView::class,
    attribute = "android:tint",
    method = "setImageTintList")])

BindingMethods是注解在类上的,例如Activity。可以包含一个到多个BindingMethod注解。BindingMethod中type表示当前方法(method)匹配到到哪个View的属性(attribute)上。

定制逻辑方法

虽然上面两者已经满足了大多数情况,但一些特殊情况还是需要自己处理逻辑的。例如,view的android:paddingLeft属性,没有setPaddingLeft(int)方法,但提供了setPadding(left, top, right, bottom)方法。这时候就需要我们自定义逻辑了。

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

BindingAdapter注解允许定制属性的setter逻辑。setPaddingLeft方法的第一个参数必须是我们要处理属性的逻辑的View,后面的参数是根据BindingAdapter注解的属性来定位的。例如这里BindingAdapter注解只声明了android:paddingLeft属性,那么参数padding就是paddigLeft对应的值。设置多个属性是这样子的:

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

<ImageView app:imageUrl="@{venue.imageUrl}"
        app:error="@{@drawable/venueError}" />

从这里可以看出,库对命名空间并没有作要求。注解的值imageUrl和error类型必须对应方法参数url和error的类型String和Drawable,只有ImageView同时匹配到两个属性,上述方法才会生效。为此,可以通过设置requireAll = false,匹配一个值也会生效。

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

类型转换

在绑定表达式返回一个对象时,库会选择一个方法来设置属性的值,而该对象会转型为方法参数的类型。这种机制可以方便使用ObservableMap来存储数据。

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

绑定表达式的userMap["lastName"]会返回值,该值会查找setText(CharSequence) 方法中自动转型为字符串并设置给TextView的text属性。但参数类型不确定的时候,就需要进行强制类型转换了,以表明类型。

有时候,绑定表达式返回的类型与设置属性方法的参数类型并不一致。例如:android:background属性期待的是Drawable(setBackground(drawable),但设置color值时确实一个Int。

<View
   android:background="@{@color/red}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

这时候我们需要使用BindingConversion注解将返回值类型Int转换成期待的类型Drawable。

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

总结

写本文的时候,参考官网,看英文文档,对一个英语刚过四级的人...词我都认识,但组成句子,我就一脸懵逼了...

写到一半的时候,想放弃,或者想一笔带过...但,说过,要打造高质量文章,和对读者负责,所以熬了几个夜...夜太黑,没人担心明天会不会后悔~

看了一下别人的文章,基本都是支持参考官网翻译的,并没有加入个人理解和筛选。而本文是在多次参考阅读官网文章之下加入个人理解,让本文更加通俗易懂,更清晰表达官网的意图。

能看到结尾的同学也是很牛逼,需要很大的耐心,给你点个👍。那能不能举个爪,让我看看你们的👐。

Data Binding还有其他知识点,我发现的英语水平已经不够用,大家可以看看原汁原味的官网,或者等到后面我再把它写完...

坚持初心,写优质好文章

开文有益,点赞支持好文

本文是Jetpack系列文章第一篇

第二篇: Jetpack:你如何管理Activity和Fragment的生命周期?

第三篇: Jetpack:在数据变化时如何优雅更新Views数据