博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Databinding 双向绑定详解
阅读量:6982 次
发布时间:2019-06-27

本文共 11097 字,大约阅读时间需要 36 分钟。

图片来自必应

Databinding是Google推出的一个支持View与ViewModel绑定的Library,可以说Databinding建立了一个UI与数据模型之间的桥梁,即UI的变化可以通知到ViewModel, ViewModel的变化同样能够通知到UI从而使UI发生改变,大大减少了之前View与Model之间的胶水代码,如findViewById, 改变及获取TextView的内容还需要调用setText()、 getText(),获取EditText编辑之后的内容需要调用getText(),而有了Databinding的双向绑定,这些重复的工作都将被省去。下面我们就来看一下如何使用Databinding来双向绑定

首先我们先定一个ViewModel,将这个ViewModel的变量content与布局文件中的TextView绑定:

class MyViewModel: ViewModel(){        var content: ObservableFiled
= ObservableFiled() }复制代码
  • 官方支持的双向绑定 这里的官方支持指的是Databinding库中已经定义了一些View的双向绑定,我们如果要使用的话只需要将xml文件中的"@{}"改成"@={}", 如下代码
(test_layout.xml):      
复制代码

上述代码中注释部分就是将单向绑定改为双向绑定的代码

官方支持的双向绑定:

  • AbsListView android:selectedItemPosition
  • CalendarView android:date
  • CompoundButton android:checked
  • DatePicker android:year, android:month, android:day
  • NumberPicker android:value
  • RadioGroup android:checkedButton
  • RatingBar android:rating
  • SeekBar android:progress
  • TabHost android:currentTab
  • TextView android:text
  • TimePicker android:hour, android:minute

那么双向绑定是怎么实现的呢?首先来看一下Databinding的源码:

首先是我们在编译之后会生成几个相关文件如test_layout.xml, test_layout-layout.xml, TestLayoutBinding.java, BR文件等。我们主要来看一下TestLayoutBinding.java这个文件。这个文件的主要作用是声明xml内的View控件以及声明的ViewModel,如本例中声明了一个TextView:

@NonNull     private final android.widget.LinearLayout mboundView0;     @NonNull     private final android.widget.TextView mboundView1;     // variables     @Nullable     private com.fb.onedayimprove.viewmodel.MyModel mModel;复制代码

上述代码中mboundView0为根布局、mboundView1为声明了双向绑定的TextView,mModel为与View绑定的ViewModel。(注意:并不是所有的变量都会被声明,有三种情况:在xml布局中声明id的,将会被定义为静态变量,可以直接通过Databinding对象访问;引用了model数据的;根布局)

接下来当我们定义了双向绑定的时候,TestLayoutBinding.java会生成这样一段代码:

private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new android.databinding.InverseBindingListener() {            @Override            public void onChange() {                // Inverse of model.content.get()                //         is model.content.set((java.lang.String) callbackArg_0)              //调用定义的TextViewBindingAdapter中的getTextString(TextView view)方法得到mboundView1(即布局中定义的TextView)的值                java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);                // localize variables for thread safety                // model                com.fb.onedayimprove.viewmodel.MyModel model = mModel;                // model.content.get()              //当前View绑定的ViewModel中的content的值                java.lang.String modelContentGet = null;                // model != null                boolean modelJavaLangObjectNull = false;                // model.content                android.databinding.ObservableField
modelContent = null; // model.content != null boolean modelContentJavaLangObjectNull = false; modelJavaLangObjectNull = (model) != (null); if (modelJavaLangObjectNull) { modelContent = model.getContent(); modelContentJavaLangObjectNull = (modelContent) != (null); if (modelContentJavaLangObjectNull) { //将View中的值取出并复制给相应的model中绑定的数据 modelContent.set(((java.lang.String) (callbackArg_0))); } } } };复制代码

通过上述代码可以看到实现逆向绑定的关键部分,可以看到调用InverseBindingListener接口的onChange()方法就可以将view的值传回ViewModel。那么它在哪里被用到呢?看下面的代码:

@Override        protected void executeBindings() {            ...            if ((dirtyFlags & 0x4L) != 0) {                android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(..., 			mboundView1androidTextAttrChanged);            }        }复制代码

可以看到在executeBindings这个方法中,将InverseBindingListener 的值传给setTextWatcher,这个方法怎么来的后面会提到。

上面的代码中提到一个TextViewBindingAdapter,通过它的名字就可以看出是绑定View与ViewModel的适配器,那么来看一下TextViewBindingAdapter做了什么 (在包android.databinding.adapters下):

@BindingAdapter("android:text")        public static void setText(TextView view, CharSequence text) {            final CharSequence oldText = view.getText();          //为防止双向绑定无限循环调用比较新旧内容是否相同            if (text == oldText || (text == null && oldText.length() == 0)) {                return;            }            if (text instanceof Spanned) {                if (text.equals(oldText)) {                    return; // No change in the spans, so don't set anything.                }            } else if (!haveContentsChanged(text, oldText)) {                return; // No content changes, so don't set anything.            }            view.setText(text);        }复制代码

上述代码相信大家并不陌生,当Databinding在给某一个控件的XXX属性赋值的时候,需要去找到相应的setXXX()方法,然后将model中的值给这个View。而@BindingAdapter ("xxx")正是将这个setXXX()方法与xxx属性关联起来,所以上面部分的代码作用总结起来就是做了view.setText(text)。(注:可以看到代码中有对新旧内容的比较,只有当内容不同的时候才会执行下一步,这是为了防止双向绑定循环调用)

再来看下一个方法:

@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")        public static String getTextString(TextView view) {            return view.getText().toString();        }复制代码

这个方法在TestLayoutBinding.java中已经说明了,是双向绑定取值时调用的方法,@InverseBindingAdapter注解和@BindingAdapter注解作用类似。

再来看下一个关键方法:

@BindingAdapter(value = {
"android:beforeTextChanged", "android:onTextChanged", "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false) public static void setTextWatcher(TextView view, final BeforeTextChanged before, final OnTextChanged on, final AfterTextChanged after, final InverseBindingListener textAttrChanged) { final TextWatcher newValue; //listener为空则不作处理 if (before == null && after == null && on == null && textAttrChanged == null) { newValue = null; } else { //创建一个TextWatcher监听text内容的变化 newValue = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if (before != null) { before.beforeTextChanged(s, start, count, after); } } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (on != null) { on.onTextChanged(s, start, before, count); } //如果设定了双向绑定,则InverseBindingListener不为空,可参见之前的TestLayoutBinding.java对其的赋值。 if (textAttrChanged != null) { //调用onChange()方法(实现在TestLayoutBinding.java中),即将view的值传回给ViewModel textAttrChanged.onChange(); } } @Override public void afterTextChanged(Editable s) { if (after != null) { after.afterTextChanged(s); } } }; } final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher); if (oldValue != null) { view.removeTextChangedListener(oldValue); } if (newValue != null) { view.addTextChangedListener(newValue); } }复制代码

从上面的代码我们可以看到@BindingAdapter绑定了几个属性,其中有一个叫做"android:textAttrChanged",那么这个属性就代表了当TextView的text属性变化,xxxAttrChanged。可以想象得到那这个方法就是在android:text属性发生变化的时候会被调用。我们在这个方法中设置了对android:text这个属性内容变化的监听(注意是内容变化,不是属性变化),以后当每次改内容变化的时候,就会调用textAttrChanged.onChange()这个方法将TextView的值传回给ViewModel。

上述的是通过官方支持的源码来看双向绑定,那么我们要自定义的View使用双向绑定应该怎么做呢?

  • 自定义View双向绑定

    通过对官方源码分析,我们发现要实现双向绑定关键是需要实现Adapter,实现setter,getter,以及Listene方法。下面就让我们来实现一个自定义View 的双向绑定吧。 对MyModel稍作修改:

class MyModel: ViewModel(){           var content: ObservableField
= ObservableField() fun onChangeClick(view: View){ if(view is TestEditView){ Log.v("---TAG---", content.get()) view.setValue("改变后") Log.v("---TAG---", "click: ${content.get()}") } } }复制代码

对test_layout.xml修改(主要是使用自定义的View),加了一个点击事件,点击之后修改View的内容:

复制代码

这里我们引入一个自定义View:这是一个类似表单的View,左边是个小标题,右边可以填一些信息等:

class TestEditView: LinearLayout{
private lateinit var contentView: TextView private var onContentedListener: OnContentChangedListener? = null { ... } private fun initView(context: Context){ val view = View.inflate(context, R.layout.test_edit_layout, this) contentView = view.findViewById(R.id.text_content) } fun setOnContentListener( listener: OnContentChangedListener?){ this.onContentedListener = listener } public fun setValue(content: String){ if (content.isEmpty() || (!content.isEmpty() && content == contentView.text)){ return } contentView.text = content onContentedListener?.onContentChanged() } fun getContent(): String{ return contentView.text.toString() } //内容改变Listener interface OnContentChangedListener{ fun onContentChanged() } }复制代码

省略了部分代码,主要就是定义了一个内容改变的Listener。 下面是TestEditViewAdapter的代码:

//使用InverbaseBindingMethod注解,与使用@BindingAdapter效果一样其中,event、method可声明也可以不声明,不声明的话会默认设置xxxAttrChanged 与 getXXX() 方法           //@InverseBindingMethods(InverseBindingMethod(type = TestEditView::class, attribute = "content", event = "contentAttrChanged", method = "getContent"))      object TestEditViewAdapter{              @JvmStatic        @BindingAdapter("app:content")        fun setContent(view: TestEditView, content: String){          if (content.isNotEmpty() && view.getContent() == content){            return          }          view.setValue(content)        }              @JvmStatic        @InverseBindingAdapter(attribute = "app:content", event = "contentAttrChanged")        fun getContent(view: TestEditView): String{          return view.getContent()        }              @JvmStatic        @BindingAdapter(value = "app:contentAttrChanged", requireAll = false)        fun setContentAttrChangedListener(view: TestEditView, bindingListener: InverseBindingListener){          if(bindingListener == null){            view.setOnContentListener(null)          }else{            //如果设置了双向绑定则为其添加监听            view.setOnContentListener(object : OnContentChangedListener{              override fun onContentChanged() {                bindingListener.onChange()              }            })          }        }      }复制代码

接下来我们只需要在Fragment中为content赋值,然后运行程序,点击TestEditView,就可以看到Log输出了content的初始值与改变后的值(不贴图展示了)。

val binding = TestLayoutBinding.inflate(inflater!!, container, false)    val model = MyModel()    model.content.set("初始值")    binding.model = model    return binding.root复制代码
  • 总结 以上就是Databinding双向绑定的使用方法,总的来说应该注意两点:

    1、修改"@{}" 为 "@={}"

    2、写setXXX(), xxxAttrChanged(), getXXX()方法。

转载地址:http://rxtpl.baihongyu.com/

你可能感兴趣的文章
Chromium 操作系统即将支持所有 SBC 单板电脑
查看>>
《CCNP安全Secure 642-637认证考试指南》——第8章 配置与实施路由式数据面安全...
查看>>
Remix OS PC 版面向全球开放
查看>>
Debian GNU/Linux 9 将切换至 GCC6 编译器
查看>>
《VMware 网络技术:原理与实践》—— 3.2 以太网
查看>>
《程序员的修炼——从优秀到卓越》一一1.6 勿以专家自居
查看>>
《Adobe Illustrator CS5中文版经典教程》—第0课0.5节使用绘图模式
查看>>
AngularJS 的自定义指令
查看>>
《CCNA ICND2(200-101)认证考试指南(第4版)》——第1章定义生成树协议
查看>>
什么样的 RPC 才是好用的 RPC
查看>>
《Adobe Premiere Pro CC经典教程》——14.6 特殊颜色效果
查看>>
Debian 项目不再提供 CD 格式的 ISO 镜像
查看>>
《设计团队协作权威指南》—第1章1.3节甘为螺丝钉
查看>>
Mozilla 将 Firefox 的命运与 Rust 语言捆绑在一起
查看>>
android 屏幕保持唤醒 不锁屏 android.permission.WAKE_LOCK
查看>>
《Unity 3D 游戏开发技术详解与典型案例》——1.3节第一个Unity 3D程序
查看>>
Airbnb数据科学团队进化论:如何由内而外实现数据驱动
查看>>
如何用机器学习预测超售,避免美联航“暴力赶客”悲剧
查看>>
css细节(实习第1天)
查看>>
腾讯Android自动化测试实战3.1.4 Robotium的控件获取、操作及断言
查看>>