美文网首页Android基础知识AndroidAndroid
Android,DataBinding的官方双向绑定

Android,DataBinding的官方双向绑定

作者: SoloHo | 来源:发表于2016-04-20 17:12 被阅读21454次

在Android Studio 2.1 Preview 3之后,官方开始支持双向绑定了。

可惜目前Google并没有在Data Binding指南里面加入这个教程,并且在整个互联网之中只有这篇文章介绍了如何使用反向绑定。

在阅读一下文章之前,我假设你已经知道如何正向绑定。

回顾一下Data Binding

在正向绑定中,我们在Layout里面的绑定表达式是这样的:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <TextView android:text="@{user.name}" .../>
  </RelativeLayout>
</layout>

当user.name的数据改动时,我们的TextView都会同步改变文字。

双向绑定

现在假设一种情况,当你更换成EditText时,如果你的用户名User.name已经绑定到EditText中,当用户输入文字的时候,你原来的user.name数据并没有同步改动,因此我们需要修改成:

<layout ...>
  <data>
    <variable type="com.example.myapp.User" name="user"/>
  </data>
  <RelativeLayout ...>
    <EditText android:text="@={user.name}" .../>
  </RelativeLayout>
</layout>

看出微小的差别了吗?对,就是"@{}"改成了"@={}",是不是很简单?

隐式引用属性

同样你也可以在别的View上引用属性:

<layout ...>
  <data>
    <import type="android.view.View"/>
  </data>
  <RelativeLayout ...>
    <CheckBox android:id="@+id/seeAds" .../>
    <ImageView android:visibility="@{seeAds.checked ? View.VISIBLE : View.GONE}" .../>
  </RelativeLayout>
</layout>

当CheckBox的状态发生改变的时候,ImageView也会同时发生改变。在复杂情况下,这个特性没什么卵用,因为逻辑部分我们是不建议写在XML中。

如何开启双向绑定

开启双向绑定,需要在项目的build.gradle中设置:

classpath 'com.android.tools.build:gradle:2.1.0-alpha3'

同样,你需要在你Module的build.gradle中设置:

android {
    ...
    dataBinding.enabled = true
}

貌似还有点问题

我们刚才的例子里面只显示了系统自带的应用,那么如果是自定义控件,或者是我们更细颗粒度的Observable呢?等下就揭晓如何自定义自己的双向绑定,我们来看看目前Android支持的控件:

  • 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

自定义双向绑定

设想一下我们使用了下拉刷新SwipeRefreshLayout控件,这个时候我们希望在加载数据的时候能控制refreshing的状态,所以我们加入了ObservableBoolean的变量swipeRefreshViewRefreshing来正向绑定数据,并且能够在用户手动下拉刷新的时候同步更新swipeRefreshViewRefreshing数据:

// SwipeRefreshLayout.java

public class SwipeRefreshLayout extends View {
    private boolean isRefreshing;
    public void setRefreshing() {/* ... */}
    public boolean isRefreshing() {/* ... */}
    public void setOnRefreshListener(OnRefreshListener listener) {
        /* ... */
    }
    public interface OnRefreshListener {
        void onRefresh();
    }
}

接下来我们需要告诉框架,我们需要将SwipeRefreshLayout的isRefreshing的值反向绑定到swipeRefreshViewRefreshing

@InverseBindingMethods({
        @InverseBindingMethod(
                type = android.support.v4.widget.SwipeRefreshLayout.class,
                attribute = "refreshing",
                event = "refreshingAttrChanged",
                method = "isRefreshing")})

这是一种简单的定义,其中event和method都不是必须的,因为系统会自动生成,写出来是为了更好地了解如何绑定的,可以参考官方文档InverseBindingMethod

当然你也可以使用另外一种写法,并且如果你的值并不是直接对应Observable的值的时候,就可以在这里进行转换:

@InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
public static boolean isRefreshing(SwipeRefreshLayout view) {
    return view.isRefreshing();
}

上面的event同样也不是必须的。以上的定义都是为了让我们能够在布局文件中使用"@={}"这个双向绑定的特性。接下来你需要告诉框架如何处理refreshingAttrChanged事件,就像处理一般的监听事件一样:

@BindingAdapter("refreshingAttrChanged")
public static void setOnRefreshListener(final SwipeRefreshLayout view,
    final InverseBindingListener refreshingAttrChanged) {
    
    if (refreshingAttrChanged == null) {
        view.setOnRefreshListener(null);
    } else {
        view.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh() {
                colorChange.onChange();
            }
        });
    }
}

一般情况下,我们都需要设置正常的OnRefreshListener,所以我们可以合并写成:

@BindingAdapter(value = {"onRefreshListener", "refreshingAttrChanged"}, requireAll = false)
public static void setOnRefreshListener(final SwipeRefreshLayout view,
    final OnRefreshListener listener,
    final InverseBindingListener refreshingAttrChanged) {

    OnRefreshListener newValue = new OnRefreshListener() {
        @Override
        public void onRefresh() {
            if (listener != null) {
                listener.onRefresh();
            }
            if (refreshingAttrChanged != null) {
                refreshingAttrChanged.onChange();
            }
        }
    };

    OnRefreshListener oldValue = ListenerUtil.trackListener(view, newValue, R.id.onRefreshListener);
    if (oldValue != null) {
        view.setOnRefreshListener(null);
    }
    view.setOnRefreshListener(newValue);
}

现在我们终于可以使用双向绑定的技术啦。但是要注意,需要设置requireAll = false,否则系统将识别不了refreshingAttrChanged属性,前文提到的文章例子里并没有设置这个。

在ViewModel中,我们的数据是这样的:

// MyViewModel.java

public final ObservableBoolean swipeRefreshViewRefreshing = new ObservableBoolean(false);

public void load() {
    swipeRefreshViewRefreshing.set(true);
    
    // 网络请求
    ....
    
    swipeRefreshViewRefreshing.set(false);
}

public SwipeRefreshLayout.OnRefreshListener onRefreshListener() {
    return new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            // Do something you need
        }
    };
}

在布局文件中是这样设置的:

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefreshListener="@{viewModel.onRefreshListener}"
    app:refreshing="@={viewModel.swipeRefreshViewRefreshing}">

    ...
</android.support.v4.widget.SwipeRefreshLayout>

最后我们还有一个小问题,就是双向绑定有可能会出现死循环,因为当你通过Listener反向设置数据时,数据也会再次发送事件给View。所以我们需要在设置一下避免死循环:

@BindingAdapter("refreshing")
public static void setRefreshing(SwipeRefreshLayout view, boolean refreshing) {
    if (refreshing != view.isRefreshing()) {
        view.setRefreshing(refreshing);
    }
}

这样就没问题啦。

相关文章

网友评论

  • 3b7065f72618:看完之后有点懵...
  • 吉凶以情迁:求大佬解决双休绑定 edittext 非字符串类型问题,由于我不太理解双休绑定,导致这个问题没解决
    asAnotherJack:我刚找到了一种解决方法,分享出来 https://stackoverflow.com/a/39010346/5924841
    asAnotherJack:请问找到解决方法了吗
    吉凶以情迁:看文章的意思看来我不能用自带的edittext了,需要继承重写。
  • A_si:OnRefreshListener oldValue = ListenerUtil.trackListener(view, newValue, R.id.onRefreshListener);这个id是在哪里引用的?这里是什么作用?id的定义有什么规则吗?
    天上下雪不下雨:@kicky android:onTextChanged 的定义在TextViewBindingAdapter 这个类中,是官方给的,你看下里面的代码就清楚了
    okylin:同问此问题解决没有?在XML里面设置正常的app:onRefreshListener之后。
    如果再在代码其他地方调用SwipeRefreshLayout.setOnRefreshListener,反向绑定会失效,这个问题怎么解决?
    EditText的android:onTextChanged绑定的回调和EditText.addTextChangedListener设置的回调就能同时生效。
  • 浅安流年:谢谢,EditText的双向绑定,解决了我的问题。
  • 5aea3ebe4fc7:你的源代码在哪里 我怎么看不懂你写的是什么 前言不搭后语的;
  • A_si:R.id.onRefreshListener 哪里有
  • A_si:看不明白。官网的这些也不太明白。attribute = "refreshing",
    event = "refreshingAttrChanged",
    method = "isRefreshing")这些命名是怎么来的?
  • b2d225618024:Android Studio 只有2.1 pre3之后的版本才支持吗,升级android gradle plugin不行吗
    我在2.0版本的上面一直不能识别@={xxx.nn}语法
  • uncochen:与前端的双向绑定框架一比,还是有很大优化的空间啊.
  • fuuuuuccccck:绑定之后,user.name的值确实会根据edittext的内容而变化了,但是,我用代码设置了user.name的值,edittext的内容不会改变了吗?这是bug还是本来就不支持这样?
    fuuuuuccccck:搞定了,要继承BaseObservable绑定一下
  • Ashen_:非常感谢
  • f0aa73a01376:@={}会报错:Error:(18, 30) Cannot find the getter for attribute 'android:text' with value type java.lang.String on android.widget.EditText.
    不知道是不是我操作的问题。
    okylin:@KiSoo AS版本需要在2.1.3之上!
    桥寻:你们怎么解决的
    8cb279ff4a4b:@aiss 我的也会出现这个问题,你解决了吗?怎么解决的。
  • SaiWu:同求demo源码,按你的步骤做,手动下拉控件刷新时候swipeRefreshViewRefreshing 的值没变化,双向绑定失败。
  • jmpfelicity:有demo嘛,
  • 我是Asha:有点难用...

本文标题:Android,DataBinding的官方双向绑定

本文链接:https://www.haomeiwen.com/subject/qsgzlttx.html