preface

This article is an outside part of the Android official architecture component series, because the current domestic blog about DataBinding bidirectional binding, it is all kinds of things, many articles are still confused after reading, I hereby write a special article to summarize.

In addition, a few days ago, I saw an article “Why DID I give up using DataBinding in the Project” published by the teacher who seems to have dropped out on CSDN. It pointed out some pain points in the current use of DataBinding, and I can sympathate with many of them. However, given the existence of things, there must be two sides. At the end of this article, I have written some personal interpretations of why I am still using DataBinding, and I hope it will be helpful to the readers.

By default, the reader of this article has a basic understanding of DataBinding.

What is two-way binding?

DataBinding itself is an observer pattern implementation of the state of the View layer. By binding the View to the ViewModel layer’s observable objects (such as LiveData), the View layer automatically updates the UI when the ViewModel layer’s data changes.

I’m talking about the most basic use of DataBinding, the unidirectional binding, which has the advantage of abstracting the View layer as a pure Java observer — which means that the ViewModel layer associated code is completely unit-testable.

But in actual development, one-way binding is not enough, in some specific scenarios, we need to use two-way binding.

For example, for a TextView display, normally we just render it with a String:

Obviously, the data flow is one-way. In other words, we assume that TextView only reads to the DataSource — if we make a network request and we need to use a property of the DataSource as a parameter, we still have no problem taking the value from the DataSource.

But in a different scenario, if we replace the TextView with an EditText, then we have to deal with something completely different, like the login screen:

This doesn’t seem to be a problem, we still bind the EditText one-way with a LiveData:

The problem occurs when we edit the input field, the UI of EditText changes, but the data in LiveData is not updated, and when we want to request the login API in the ViewModel layer, We have to go through edittext.gettext () to get the password that the user entered.

So we want the data in The LiveData to stay in sync with the EditText content even if the EditText content changes — so that we don’t need the ViewModel layer to hold a reference to the View layer, and when we request the interface, we just take the value directly from the LiveData:

That’s what bidirectional binding is all about.

What are the usage scenarios

What works for bidirectional binding? Remember the sentence above:

For a one-way binding, the data flow is one-way. In other words, we assume that TextView only reads to the DataSource.

Now we define that when an undefined action occurs — typically, this action represents user interaction with UI controls — the UI changes need to affect the data state in the ViewModel layer (in addition to data-driven views, views also drive data to facilitate future network requests and so on), This is where two-way binding comes in.

Obviously, EditText is one of the classic usage scenarios for bidirectional binding. In addition, bidirectional binding is very common, such as CheckBox:

When the user selects the CheckBox, we certainly expect the LiveData

status of the ViewModel layer to be updated accordingly, so that we can make network requests directly from the LiveData values as parameters in the future.

Two-way binding, and if there is no user operation the UI, we will need to manually add code to ensure that state synchronization — such as the checkBox. SetOnCheckChangedListener (), otherwise, you’ll get with the expected within the next operating different results.

That sounds like a lot of trouble, but how exactly does it work?

Fortunately, DataBinding has already implemented most of the bidirectional bindings used in Android native controls:

This means that we don’t need to implement the complex bidirectional binding manually. In the example of EditText above, we just need to use @={expression} to do the bidirectional binding:

<EditText
	android:id="@+id/etPassword"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:text="@={ fragment.viewModel.password }" />
Copy the code

Instead of one-way binding, you only need one more = symbol to keep the state of the View layer and ViewModel layer in sync.

What’s the hard part?

Once defined, the bidirectional binding is easy to use, but it is slightly more difficult to define than the one-way binding. Even though the native control DataBinding has helped us implement it, for the three-party control or custom control, we need to implement it ourselves.

Taking SwipeRefreshLayout as an example, let’s take a look at the way it is implemented:

object SwipeRefreshLayoutBinding {

    @JvmStatic
    @BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
    fun setSwipeRefreshLayoutRefreshing(
            swipeRefreshLayout: SwipeRefreshLayout,
            newValue: Boolean
    ) {
        if(swipeRefreshLayout.isRefreshing ! = newValue) swipeRefreshLayout.isRefreshing = newValue }@JvmStatic
    @InverseBindingAdapter(
            attribute = "app:bind_swipeRefreshLayout_refreshing",
            event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"
    )
    fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
            swipeRefreshLayout.isRefreshing

    @JvmStatic
    @BindingAdapter(
            "app:bind_swipeRefreshLayout_refreshingAttrChanged",
            requireAll = false
    )
    fun setOnRefreshListener(
            swipeRefreshLayout: SwipeRefreshLayout,
            bindingListener: InverseBindingListener?). {
        if(bindingListener ! =null)
            swipeRefreshLayout.setOnRefreshListener {
                bindingListener.onChange()
            }
    }
}
Copy the code

A little obscure, isn’t it? Let’s not get bogged down in the details of the implementation, but take a look at how it works in code:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		app:bind_swipeRefreshLayout_refreshing="@={ fragment.viewModel.refreshing }">

            <androidx.recyclerview.widget.RecyclerView/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Copy the code

Refreshing actually is just a LiveData:

val refreshing: MutableLiveData<Boolean> = MutableLiveData()
Copy the code

The bidirectional binding here means that when we manually set values for LiveData, SwipeRefreshLayout’s UI changes accordingly. Similarly, when the user manually pulls down to perform the refresh operation, the value of LiveData will correspondingly change to true(indicating the status in refresh).

In contrast to other methods, bidirectional binding abstractions SwipeRefreshLayout’s refresh status as a LiveData

— we only need to define it in THE XML, and then we can code around this status in the ViewModel. Is different from the setOnRefreshListener () the way, this is pure Java code, we can have a pure JVM for every line of code of unit tests.

All of the code in this section is available here.

Organize ideas, step by step to achieve two-way binding

Having said all this, we haven’t implemented a single line of code yet. Don’t worry, because coding is just one step. The most important thing is to organize a smooth idea, so that, in the next coding phase, you will be a god.

1. Implement one-way binding

As we know, the premise of bidirectional binding is unidirectional binding. Therefore, we first configure the interface corresponding to unidirectional binding:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing(
        swipeRefreshLayout: SwipeRefreshLayout,
        newValue: Boolean
) {
        swipeRefreshLayout.isRefreshing = newValue
}
Copy the code

By binding the LiveData values to the DataBinding, we update SwipeRefreshLayout’s refresh status whenever the LiveData status changes.

So now that we have a data-driven view, the next thing we need to think about is, how do we know that the user is going to do the drop down?

2. Observe the status change of the View layer

Only observe the condition changes the View layer, we can drive LiveData for corresponding updates, actually very simple, through swipeRefreshlayout. SetOnRefreshListener (a) :

@JvmStatic
@BindingAdapter(
        "app:bind_swipeRefreshLayout_refreshingAttrChanged",
        requireAll = false
)
fun setOnRefreshListener(
        swipeRefreshLayout: SwipeRefreshLayout,
        bindingListener: InverseBindingListener?). {
    if(bindingListener ! =null)
        swipeRefreshLayout.setOnRefreshListener {
            bindingListener.onChange()   / / 1}}Copy the code

InverseBindingListener () notifying the DataBinding whenever the swipeRefreshLayout update status is changed by the user’s action:

A: hi! The status of the View layer has changed, you need to notify LiveData to update the corresponding data.

Now that the DataBinding knows it needs to notify LiveData to update the data, the key is —

3. What data do I give to LiveData?

Yes, even though LiveData needs to update, it doesn’t know what the new status is.

LiveData: Dude, give me the data!

We need to update The status of SwipeRefreshLayout to LiveData, so we use the InverseBindingAdapter comment to connect to step 2:

@JvmStatic
@InverseBindingAdapter(
        attribute = "app:bind_swipeRefreshLayout_refreshing",
        event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"// 2 [Attention!] )
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
        swipeRefreshLayout.isRefreshing
Copy the code

Note that the line of code in the //2 comment does not, we pass the same tag (app:bind_swipeRefreshLayout_refreshingAttrChanged, which we declared in Step 2), Form a binding docking with the code block in Step 2.

Now, LiveData knows how to do data updates in reverse:

Each time the user dropdown refresh, inform DataBinding InverseBindingListener, LiveData from swipeRefreshLayout. IsRefreshing know the latest status, and data synchronization update.

4. Don’t forget to prevent infinite loops!

If you are careful, you can already sense something is wrong. The current bidirectional binding has a fatal problem, which is the ANR anomaly caused by the infinite loop.

When the state of the View UI changes, the ViewModel is updated, which in turn notifies the View layer to refresh the UI, which in turn notifies the ViewModel to update…….

Therefore, in order to ensure that an infinite loop does not cause an ANR exception to occur in the App, we need to add a judgment in the initial code block to ensure that the UI will only be updated if the state of the View changes:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing(
        swipeRefreshLayout: SwipeRefreshLayout,
        newValue: Boolean
) {
    if(swipeRefreshLayout.isRefreshing ! = newValue)// Update the UI only when the new and old states are different
        swipeRefreshLayout.isRefreshing = newValue
}
Copy the code

Summary: Why do I still stick to DataBinding

In the original plan of this article, there was also a module on source analysis of two-way binding, which was later decided to be unnecessary, because even the source code was just a verbose restatement of the idea of implementation in the previous article.

Bidirectional binding is a highly controversial feature in itself; In fact, DataBinding itself is controversial — it doesn’t matter whether DataBinding is good or not, whether it’s used or not, but what matters is that we need to look at the ideas it represents: That is, how do you take a hard-to-test, state-changing View and abstract it through code into a pure Java state that is easy to maintain and test?

DataBinding’s greatest advantage is that it abstractions the annoying View layer code into an easy-to-maintain data state, while greatly reducing the glue code that the View layer abstractions into the ViewModel layer.

Of course, DataBinding is not necessarily the right solution. In fact, RxBinding is another great solution. I can still abstract it as an Observable

— the former by binding and observing data in XML, the latter by abstrbehaving the state of the View into a stream in RxJava, but in the end, the two ideologize the same thing.

series

Create the best series of Android Jetpack blogs:

  • A detailed analysis of the official Android architecture component Lifecycle
  • Android’s official architecture component ViewModel: From the past to the present
  • LiveData, the official Android architecture component: Two or three things in the Observer pattern area
  • Paging: The design aesthetics of the Paging library, an official Android architecture component
  • Official Android architecture component Paging-Ex: Adds a Header and Footer to the Paging list
  • Official Architecture component of Android Paging-Ex: reactive management of list states
  • Navigation: a handy Fragment management framework
  • DataBinding-Ex is an official Architecture component for Android

Jetpack for Android

  • Open source project: Github client implemented by MVVM+Jetpack
  • Open source project: Github client based on MVVM, MVI+Jetpack implementation
  • Summary: Using MVVM to try to develop a Github client and some thoughts on programming

About me

If you think this article is of value to you, please feel free to follow me at ❤️ or on my personal blog or Github.

If you think the writing is a little bit worse, please pay attention and push me to write a better writing — just in case I do someday.

  • My Android learning system
  • About article correction
  • About Paying for Knowledge