Android,DataBinding的官方雙向綁定

轉載自:http://www.jianshu.com/p/c481d1f4e0b6

在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);
    }
}

這樣就沒問題啦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章