【Jetpack】学穿:DataBinding → 数据绑定 (使用篇)(下)

简介: 前面的章节 《【Jetpack】学穿:ViewBinding → 视图绑定》 剥源码的时候就有看到 DataBinding 相关的代码。 ViewBinding(视图绑定) 的作用和原理一言以蔽之: 作用 → 代替findViewById 的同时,还能保证 空安全 和 类型安全,且 支持Java; 原理 → AGP为模块中的每个XML生成绑定类,本质上还是findViewByid,只是自动生成控件实例,并一一对应;

6) 高级绑定


动态变量,有时系统并不知道特定的绑定类,但仍需指定绑定值,如RecyclerView.Adapter,示例如下:


class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    lateinit var binding: ViewDataBinding
}
class MyAdapter(data: List<User>) : RecyclerView.Adapter<BindingHolder>() {
    private var mData = data
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
        // 核心代码
        val binding: ViewDataBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_layout,
            parent,
            false
        )
        val holder = BindingHolder(binding.root)
        holder.binding = binding
        return holder
    }
    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        val user = mData[position]
        holder.binding.setVariable(BR.mUser, user)
    }
    override fun getItemCount() = mData.size
}


④ 绑定适配器


1) 自动选择方法


属性搜索对应方法,不会考虑命名空间,只考虑 属性名称 和 **类型**,如:


android:text="@{user.name}


如果user.getName()的返回值为String,查找接受String参数的setText()方法,所以表达式返回正确的类型很重要,必要时你还可以根据需要进行类型转换。


2) 指定自定义方法名


使用 @BindingMethods 注解一个类 (接口也可以),相当于一个容器,内部参数是一个 @BindingMethod 数组。一般用不到它,绝大部分的属性DataBinding都已经使用命名惯例实现了。用法示例如下:


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


3) 提供自定义逻辑


有些属性需要自定义逻辑,可以使用 @BindingAdapter 注解来自定义setter的,如:android:paddingLeft没有关联的setter,而是提供了setPadding(left, top, right, bottom) 。示例如下:


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


注意参数类型:与属性关联的View类型 + 与属性绑定表达式中接受的类型


还可以定义接收多个属性的适配器,示例如下:


@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}" />


如果ImageView同时使用了imageUrl、error,且前者是String,后者是Drawable,就会调用适配器。


如果你希望设置了任意属性就调用适配器,可以将适配器的 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);
    }
}



  • requireAll设置为false,没填写的属性将为null,需要做好非空判断!
  • 上述写法命名空间可以随意,写xxx:imageUrl也是可以的,但如果定义了如android:imageUrl就只能用这个命名空间;
  • 自定义的绑定适配器和默认数据绑定适配器冲突,会使用自定义的绑定适配器;
  • @BindingMethod属性名和@BindingAdapter定义的属性名相同会冲突报错;

官方文档还贴出了更复杂一点的示例,读者感兴趣自己看吧,懒得搬运了~


4) 对象转换


自动转换对象:绑定表达式返回Object时,会选择用于设置属性值的方法,会自动转换为所选方法的参数类型。


自定义转换:某些情况下,需要在特定类型间自定义转换,如 android:background 需要 Drawable 但指定color传入的值却是整数。


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


每当需要Drawable且返回整数时,int都应转换为ColorDrawable,可以使用 @BindingConversion 注解静态方法来完成这个转换。


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


注:绑定表达式提供的值类型要保持一致,不能在同一个表达式中使用不同类型,如:


<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>


⑤ 双向数据绑定


单向数据绑定时,可为属性设置值,并在事件监听器中更新属性:


<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>


双向数据绑定为上述过程提供了一种快捷方式:


<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>


相比起普通的@{}多了个**=**,可接收属性的数据更改并同时监听用户更新,对应属性还得做下更改:


class LoginViewModel : BaseObservable {
    // val data = ...
    @Bindable
    fun getRememberMe(): Boolean {
        return data.rememberMe
    }
    fun setRememberMe(value: Boolean) {
        // 避免死循环
        if (data.rememberMe != value) {
            data.rememberMe = value
            // 对变化做出反应
            saveData()
            // 更新观察者
            notifyPropertyChanged(BR.remember_me)
        }
    }
}


由于可绑定属性的 getter 方法称为 getRememberMe(),因此属性的相应 setter 方法会自动使用名称 setRememberMe()


双向绑定 存在一个很大的问题 死循环,数据变化触发视图变化,视图变化又会触发数据变化,一直循环,所以 需要对变化前后的数据进行判断,有变动才更新。


DataBinding中内置支持双向绑定的类如下图所示:


网络异常,图片无法展示
|


表中没有的属性,想用双向绑定,就得自己实现 @BindingAdapter 注解了。


1) 自定义属性的双向绑定


官方例子:对名为MyView的自定义View中,对其"time"属性启用双向绑定,流程如下:


// 1、使用@BindingAdapter修饰setter方法
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
    // 新旧值对比,避免死循环
    if (view.time != newValue) {
        view.time = newValue
    }
}
// 2、使用@InverseBindingAdapter修饰getter方法
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
    return view.getTime()
}


DataBinding知道 数据更改时要执行的操作(@BindingAdapter注解修饰的方法),还知道 View属性发生改变时要调用的内容(InverseBindingListener),但不知道属性何时被修改,所以还要给View设置监听器,将@BindingAdapter注解也加到监听器方法上:


// 3、View上设置监听器,可以是自定义的,也可以是通用事件,如焦点丢失或文本修改
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
        view: MyView,
        attrChange: InverseBindingListener
) {
    // Set a listener for click, focus, touch, etc.
    attrChange.onChange() // 通知数据更新 
}


监听器中包含一个 InverseBindingListener 可用它告知DataBinding,属性已更改,可以开始调用 @InverseBindingAdapter 修饰的方法。


2) 转换器


绑定到View的变量需要设置格式、转换或更改后才能显示,可以定义转换器对象来设置格式。


如果使用到双向表达式,还得使用反向转换器,以告知DataBinding如何将用户提供的字符串转换回后备数据类型。示例如下:


object Converter {
    // 添加注解修饰反向转换器。
    @InverseMethod("stringToDate")
    @JvmStatic fun dateToString(
        view: EditText, oldValue: Long,
        value: Long
    ): String {
        // Converts long to String.
    }
    @JvmStatic fun stringToDate(
        view: EditText, oldValue: String,
        value: String
    ): Long {
        // Converts String to long.
    }
}


纸上得来终觉浅,绝知此事要躬行,大概的用法就过到这里,后续实践过程遇到问题再来补充 (如和其他Jetpack组件配合)。


0x4、妙用DataBinding——解决Drawable复用


Android日常开发中,有一项令我们头大的"小事" → drawable.xml文件的维护,怎么说?


  • 没有固定的设计规范,不同的设计师有不同的颜色、圆角大小倾向;
  • 祖传代码,每个接盘开发仔,都有自己一套命名规则,有些文件内容一样,就是名字不一样;


来来来,看看公司项目中drawable的这些命名:


网络异常,图片无法展示
|


谁看了不头皮发麻啊,还维护个XX,最好的维护就是不维护,上来就新建:


写个脚本扫描下项目中drawable.xml的文件个数 (基于Python):


import os
def search_all_drawable(path):
    global drawable_count
    os.chdir(path)
    items = os.listdir(os.curdir)
    for item in items:
        contact_path = os.path.join(path, item)
        // 判断文件夹、路径包含\drawable\的文件、不满足条件的文件
        if os.path.isdir(contact_path):
            print("[-]", contact_path)
            search_all_drawable(contact_path)
        elif contact_path.find(drawable_sep) != -1:
            if contact_path.endswith(".xml"):
                print("[+]", contact_path)
                drawable_count += 1
        else:
            print('[!]', contact_path)
            pass
if __name__ == '__main__':
    drawable_sep = os.path.sep + "drawable" + os.path.sep
    drawable_count = 0
    search_all_drawable(r"D:\Code\Android\项目路径")
    print("检索到drawable.xml文件共计:%d 个" % drawable_count)


运行结果如下:


网络异常,图片无法展示
|


817个,一个300多字节,算3个1KB好了,如果能全部干掉的话,能减少273KB的体积,APK瘦身新技能get√


① 笔者已知干掉drawable.xml的两种思路


自定义View


思路:将drawable.xml中的常用属性作为控件的自定义属性,在内部动态生成Drawable作为控件的背景。


实现示例Silhouette


网络异常,图片无法展示
|


就是对常用到drawable的控件进行自定义封装,可以,但侵入式太强了,不好应用到其他任意控件。有些第三方控件可能还得走drawable.xml的老路。


代码自动生成Drawable赋值给控件


一种实现方法:手动构建GradientDrawable,配合扩展函数、扩展属性等语法特性,动态设置。


实现示例《干掉shape,手动构建GradientDrwable》


通过下面这样的代码动态设置


mBinding.goMeetingBtn.shape = corner(17) +
    stroke(5, "#ff0000") +
    gradient(GradientDrawable.Orientation.RIGHT_LEFT, "#00ff00", "#0000ff")


还可以再折腾下,弄成更简洁的DSL形式,感兴趣可以参考 drawable.dsl 自行实现。


另一种实现方法:为LayoutInflater添加自定义LayoutInflater.Factory,解析添加的自定义属性,并生成系统提供的GradientDrawable、RippleDrawable、

StateListDrawable。


实现示例BackgroundLibrary原理解读《无需自定义View,彻底解放shape,selector吧》


第二种思路的实现相比第一种侵入性低多了,接着看看用DataBinding怎么做~


② 用DataBinding干掉drawable.xml的思路


上面说过,可以通过 @BindingAdapter 注解为属性提供自定义逻辑。


我们要做的就是抽取drawable.xml中的常用属性,定下属性命名规则,如:drawable_solidColor,然后编写Drawable创建及设置的逻辑,示例如下:


@BindingAdapter(value = {
        "drawable_solidColor",
        "drawable_radius",
}, requireAll = false)
public static void setViewBackground(View v, int color, int radius) {
    GradientDrawable drawable = new GradientDrawable();
    drawable.setColor(color);
    drawable.setCornerRadius(radius);
    view.setBackground(drawable);
}


接着就可以在符合DataBinding规则的xml中使用了:


<layout>
    <TextView
        drawable_radius="@{10}"
        drawable_solidColor="@{0xffff0000}"
        android:layout_width="60dp"
        android:layout_height="60dp" />
<layout/>


原理还是非常简单的,就是把常用的属性抠出来比较麻烦,有轮子直接扒:noDrawable


网络异常,图片无法展示
|


不想另外依赖库的话,直接Copy这两个文件就好:


网络异常,图片无法展示
|


还可以根据自己的需求添加属性,或进行其他扩展,这种方案也比较简单。不过也有局限性,需要对应的 页面用上DataBinding,否则不会生效。


这些方案对于新项目还好,旧项目的话,想一上来就干掉所有drawable.xml,不太现实,重复的工作量太大了。可以先保证新开发的页面不再使用drawable.xml,后续改动到的页面逐步去掉drawable.xml,当剩余drawable.xml量级比较少时再批量修改。没有最好的方案,只有最适合的方案。


2333,想批量干掉也不是不可以,写脚本就好,毕竟都是重复操作,BackgroundLibrary那个方案比较好入手,文件文本替换。大概的思路:定义一个类存每个Drawable对应属性的值,然后遍历所有drawable.xml,查找用到@drawable/xxx的xml文件,定位到对应标签,删掉原来的语句,补上BackgroundLibrary里定义的属性和值。另外,如果Java/Kotlin代码中动态用到了drawable.xml还得单独处理下。


0x5、小结


本节系统地过了一下DataBinding的用法,还送了一个DataBinding解决Drawable复用的案例,相信读者看完应该能够放心大胆地用上DataBinding了。


使用过程遇到的问题及解决方案欢迎在评论区反馈,笔者自己也会记录补充下,原理篇先欠着,后续有时间再填,就酱,谢谢~


参考文献:



相关文章
|
9月前
|
Android开发
Android JetPack组件之DataBinding的使用详解
Android JetPack组件之DataBinding的使用详解
182 0
|
API Android开发 Kotlin
【Jetpack】学穿:Activity Results API(下)
【Jetpack】学穿:Activity Results API
312 0
|
API 开发者
【Jetpack】学穿:Activity Results API(中)
【Jetpack】学穿:Activity Results API
197 0
|
API 开发者
【Jetpack】学穿:Activity Results API(上)
【Jetpack】学穿:Activity Results API
176 0
|
存储 缓存 数据管理
【Jetpack】学穿:ViewModel → 视图模型(下)
本节带来组件 → ViewModel 视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态
235 0
|
XML 数据格式 Python
【Jetpack】学穿:ViewModel → 视图模型(中)
本节带来组件 → ViewModel 视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态
142 0
|
存储 编解码 数据管理
【Jetpack】学穿:ViewModel → 视图模型(上)
本节带来组件 → ViewModel 视图模型的解读!叫 视图数据 可能更贴切,有人也叫 视图状态
165 0
|
缓存 Java 编译器
【Jetpack】学穿:LiveData → ???(下)
在开始这篇文章前,我就遇到了第一个关于LiveData的问题:该怎么翻译这个词呢?
268 0
【Jetpack】学穿:LiveData → ???(中)
在开始这篇文章前,我就遇到了第一个关于LiveData的问题:该怎么翻译这个词呢?
142 0
【Jetpack】学穿:LiveData → ???(上)
在开始这篇文章前,我就遇到了第一个关于LiveData的问题:该怎么翻译这个词呢?
144 0