This article continues with more advanced topics, including list binding, custom properties, bidirectional binding, expression chains, Lambda expressions, animation, Component injection (testing), and more.
Demo source library: DataBindingSample.
The list of binding
List display is often used in App. Data Binding can also play an important role in lists, directly Binding Data and events to each list item.
RecyclerView
In the past, we used to use a ListView, GridView, or some custom View on GitHub for waterfall streams. Since RecyclerView came along, we have a new option, just use LayoutManager. RecyclerView built-in garbage collection, ViewHolder and ItemDecoration mechanisms allow us to replace the original ListView and GridView without hesitation.
So this article only take RecyclerView as an example.
Generic Binding
We just need to define a base class ViewHolder to easily use a Data Binding:
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected final T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding(a) {
returnmBinding; }}Copy the code
The Adapter can use the ViewHolder directly, or it can inherit the ViewHolder, using the item-specific Binding class (for direct access to the internal View). As for the Listener, you can bind it in onBindViewHolder, like a regular View, without going into details.
Since the same Adapter may not have only one ViewHolder, there may be several View types, so in onBindViewHolder, we can only get the ViewHolder type of the base class, namely BindingViewHolder, So you can’t do specific set operations like setEmployee. You can use the setVariable interface and specify the name of the variable via BR.
For example, we may have multiple view types corresponding to XML. We can write all corresponding variable names as items to avoid casting Binding classes to perform set operations. Similarly, listeners can all be named listener or Presenter.
Open source solutions and their limitations
evant / binding-collection-adapter radzio / android-data-binding-recyclerview
Both provide simplified RV data binding schemes.
The former can directly set the corresponding items and itemView in the RV of the layout, also support a variety of view types, and can directly set the corresponding LayoutManager.
The latter similarly provides the ability to bind items and itemView directly to RV in XML.
Compared to the former function is a little more powerful. However, these open source libraries correspondingly lose flexibility, viewModels need to follow the specification, and the binding of events is rigid, which is not as powerful as the inherited Adapter. The only benefit is that you write less code.
Custom attributes
In the default Android namespace, not all properties can be set directly via data Binding, such as margin, padding, and custom View properties.
When we encounter these attributes, we need to define their binding methods ourselves.
Setter
Just as the Data Binding automatically looks for the get method, it will also automatically look for the corresponding set method when encountering the property Binding.
Take DrawerLayout as an example:
<android.support.v4.widget.DrawerLayout
android:layout_width="Wrap_content"
android:layout_height="Wrap_content"
app:scrimColor="@ {@ color/scrimColor}" />Copy the code
So, by using the app namespace, the data Binding will find the corresponding set method based on the property name, scrimColor -> setScrimColor:
public void setScrimColor(@ColorInt int color) {
mScrimColor = color;
invalidate();
}Copy the code
If not, an error is reported at compile time.
Using this feature, we can inherit some third party custom views and add our set function to them to use data binding.
For example, with Fresco’s SimpleDraweeView, if we want to specify the URL directly in XML, we can add:
public void setUrl(String url) {
view.setImageURI(TextUtils.isEmpty(url) ? null : Uri.parse(url));
}Copy the code
This way, you can bind the url of the image directly in the XML. Wouldn’t that be a bit of a hassle, and if you have some system views, why would you want to inherit them and use your own classes? No, there are other ways to customize property bindings.
BindingMethods
What if the View itself supports a set of such attributes, but the attribute names in the XML are different from the method names in the Java code? Do we have to inherit the View and make the code redundant just for that?
Of course, it’s not that stupid, so we can use BindingMethods annotations instead.
Android: Tint is a color attribute for ImageView that can change the color of an icon without changing the image. If we use data binding directly on Android :tint, we will compile an error because we will look for setTint methods that do not exist. The actual method is setImageTintList.
We can use BindingMethod to specify the BindingMethod of the attribute:
@BindingMethods({
@BindingMethod(Type = "android.widget.ImageView", attribute = "Android :tint", method = "setImageTintList"),}Copy the code
We could also rename the Setter by calling BindingMethod.
BindingAdapter
What if there is no corresponding set method, or the method signature is different? The BindingAdapter annotation helps us do this.
For example, the Android :paddingLeft property of the View does not have a direct setting method, just setPadding(left, top, right, bottom), There is no way to inherit and modify this basic View to use a Data Binding (even if we did, there would still be a bunch of views that inherit from it). For those margins, you have to get LayoutParams to modify them, which can’t be done through simple set methods.
In this case, we can define a static method using the BindingAdapter:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}Copy the code
In fact the Adapter has been implemented by Data Binding is good, can in the android. Databinding. Adapters. ViewBindingAdapter see there are a lot of defined Adapter, and BindingMethod. If you need to write something else, just follow these.
We can also do multi-attribute binding, for example
@BindingAdapter({"bind:imageUrl"."bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}Copy the code
To use Picasso to read the image into ImageView.
BindingConversion
Sometimes the properties that we want to bind in XML are not necessarily what the final set method needs, like we want color (int), but the view needs a Drawable, like we want String, and the view needs a Url. At this point we can use BindingConversion:
<View
android:background="@ {isError ? @color/red : @color/white}"android:layout_width="Wrap_content"
android:layout_height="Wrap_content" />Copy the code
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}Copy the code
Two-way binding
Custom Listener
In the past, we needed to define our own Listener to do bidirectional binding:
<EditText android:text="@ {user. The name}"
android:afterTextChanged="@ {callback. Change}" />Copy the code
public void change(Editable s) {
final String text = s.toString();
if (!text.equals(name.get()) {
name.set(text);
}
}Copy the code
Bind the afterTextChanged method yourself and check if the text has changed and modify observable if it has.
New way – @=
It is now possible to use @= (instead of @) directly for bidirectional binding, which is very simple to use
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:text="@={model.name}"/>Copy the code
This way, our input to the EditText is automatically set to the name field of the corresponding Model.
The principle of
InverseBindingListener
InverseBindingListener is the listener that is triggered when an event occurs:
public interface InverseBindingListener {
void onChange(a);
}Copy the code
Observables, such as TextWatcher and OnCheckedChange, are notified indirectly through this interface. For example, EditText. InverseBindingListener
private android.databinding.InverseBindingListener mboundView1androidTe = new android.databinding.InverseBindingListener() {
@Override
public void onChange(a) {
// Inverse of model.name
// is model.setName((java.lang.String) callbackArg_0)
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
// localize variables for thread safety
// model ! = null
boolean modelObjectnull = false;
// model
com.github.markzhai.sample.FormModel model = mModel;
// model.name
java.lang.String nameModel = null; modelObjectnull = (model) ! = (null);
if(modelObjectnull) { model.setName((java.lang.String) (callbackArg_0)); }}};Copy the code
InverseBindingMethod & InverseBindingAdapter
The generated code above, we can see the code through TextViewBindingAdapter. GetTextString (mboundView1) to get the string in the EditText, view the source code can be seen
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}Copy the code
The BindingMethod and BindingAdapter perform set operations in the same way that the BindingMethod and BindingAdapter perform SET operations.
The complete logic is again:
@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);
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
@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;
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
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);
}
if(textAttrChanged ! =null) { 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); }}Copy the code
InverseBindingMethod can be used to do the same:
@InverseBindingMethods({
@InverseBindingMethod(type = android widget. The TextView. Class, attribute = "android: text", method = "getText",// Get is obtained by default based on attribute nameEvent = "android: textAttrChanged")})// Add AttrChanged by attribute by defaultCopy the code
The data Binding finds the setTextWatcher method via the textAttrChanged event, and the setTextWatcher tells the onChange method of the InverseBindingListener, The onChange method uses the found get and set methods to check and update.
Solve the loop
If you think about the logic of bidirectional binding, user input causes instance events that update the properties of the instance, and that property change triggers notify of the View, creating an endless loop of refreshing each other.
In order to solve the problem of infinite loops, we need to do a simple check. In the setText method above, we can see that if the text has not changed, it will return directly, thus eliminating the possibility of infinite loops. Keep this in mind when doing your own custom bidirectional binding.
Currently, only binders such as Text, Checked, Year, Month, Hour, rating, and progress are supported.
Property change monitor
What if we want to do something else besides update An Observable? Like updating flag bits based on input? We can be used directly on the observables addOnPropertyChangedCallback method:
mModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
if (i == BR.name) {
Toast.makeText(TwoWayActivity.this."name changed",
Toast.LENGTH_SHORT).show();
} else if (i == BR.password) {
Toast.makeText(TwoWayActivity.this."password changed", Toast.LENGTH_SHORT).show(); }}});Copy the code
Expression chain
Repeated expression
<ImageView android:visibility="@ {user. IsAdult ? View.VISIBLE : View.GONE} "/ >
<TextView android:visibility="@ {user. IsAdult ? View.VISIBLE : View.GONE} "/ >
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>Copy the code
It can be simplified as:
<ImageView android:id="@ + id/avatar"
android:visibility="@ {user. IsAdult ? View.VISIBLE : View.GONE} "/ >
<TextView android:visibility="@ {avatar. Visibility}" />
<CheckBox android:visibility="@{avatar.visibility}"/>Copy the code
Implicit update
<CheckBox android:id="/" @ + id/seeAds>
<ImageView android:visibility="@ {seeAds. Checked ?
View.VISIBLE : View.GONE} "/ >Copy the code
The ImageView will automatically change visibility when the CheckBox status changes.
Lambda expressions
Instead of using a method reference directly to write a method in Presenter with the same arguments as OnClickListener, we can use Lambda expressions:
The android: onClick = "@ {(view) - > presenter. Save (view, Item)} "the android: onClick =" @ {() - > presenter. Save (item)} "android: onFocusChange =" @ {(v, FCS) - > presenter. Refresh (item)}"Copy the code
We can also reference the View ID in lambda expressions (as in the expression chain above), as well as the context.
animation
transition
With a Data Binding, we can also animate the transition automatically:
binding.addOnRebindCallback(new OnRebindCallback() {
@Override
public boolean onPreBind(ViewDataBinding binding) {
ViewGroup sceneRoot = (ViewGroup) binding.getRoot();
TransitionManager.beginDelayedTransition(sceneRoot);
return true; }});Copy the code
So we can see some transition animations when our view changes, for example, when our visibility changes.
Component injection
What if we want to do some testing with data Binding? Like making notes, writing things down:
public class MyBindingAdapters {
@BindingAdapter(" android: text ")public static void setText(TextView view, String value) {
if (isTesting) {
doTesting(view, value);
} else {
TextViewBindingAdapter.setText(view, value)
}
}
}Copy the code
But then we have to write if/else for all methods, which is very difficult to maintain and aesthetic.
Then we can use component:
public class MyBindingAdapters {
@BindingAdapter(" android: text ")public static void setText(TextView view, String value) {
if (isTesting) {
doTesting(view, value);
} else {
TextViewBindingAdapter.setText(view, value)
}
}
}
public class TestBindingAdapter extends MyBindingAdapters {
@Override
public void setText(TextView view, String value) { doTesting(view, value); }}public interface DataBindingComponent {
MyBindingAdapter getMyBindingAdapter(a);
}
public TestComponent implements DataBindingComponent {
private MyBindingAdapter mAdapter = new TestBindingAdapters();
public MyBindingAdapter getMyBindingAdapter(a) {
returnmAdapter; }}Copy the code
What about static adapter? We just need to take Component as the first argument:
@BindingAdapter(" the android: SRC ")public static void loadImage(TestComponent component, ImageView view, String url) {
/ / /...
}Copy the code
Finally, DataBindingUtil. SetDefaultComponent (new TestComponent ()); You can make a Data Binding use the Adapter method provided by the Component.
Learn and use advice
Study suggest
- Try as much as you can in a project, and only when you continue to encounter business requirements will you use the Data Binding in real situations and discover its power.
- Feel the boundaries between XML and Java. Don’t assume that Data Binding is everything and try to write logic in XML. If your colleagues can’t see what the expression does ata glance, it probably should be in Java code, in the form of a ViewModel that takes care of some of the logic.
- Advanced Data Binding features such as Lambda expressions/test-time injection can also be tried on your own, especially injection, which is quite powerful.
Use advice
- Don’t hesitate to jump on a new project.
- For older projects, you can replace libraries like ButterKnife, starting with findViewById, and gradually replacing older code.
- Callback bindings only do event passing, NO business logic, such as transfers
- Keep expressions simple (don’t do overly complex strings, function calls)
For old projects, the following step-by-step replacements can be made:
Level 1 – No more findViewById
Replace findViewById step by step. Instead, use binding.name, binding.age to access the View directly.
Level 2 – SetVariable
To introduce variable, replace the View set manually in code with XML reference to variable directly.
Level 3 – Callback
Use the Presenter/Handler class for event binding.
Level 4 – Observable
Create a ViewModel class for instant property updates that trigger a UI refresh.
Level 5 – Bidirectional binding
Use bidirectional binding to simplify form logic and turn form data into ObservableField. This also allows us to do cool things in XML, such as the button being enabled only if all fields are not empty (which used to require several EditText OnTextChange listeners).
conclusion
The first and second chapters of this article introduce most of the existing features of data Binding and some of the implementation principles. If you just look at them and don’t put them into practice, you may feel a bit overdone. It is recommended that you put them into practice through projects to truly appreciate the power of data Binding. Welcome to join our QQ group (568863373) for discussion, you can also add my wechat (shin_87224330) to study together.