Effective Power of Android Data-Binding
In the context of this year's Google I/O, we finally heard that Google promises not to leave developers without attention this time. Have they finally understood that the easier it will be to develop and more best-practice tutorials & technologies there are the more high-quality applications there will be for Google to earn on? The question is rhetorical. Will Google be able to introduce something new and at the same time to maintain flexibility, which is sometimes not enough in iOS development? Let's see.
Data-binding in Android is the ability to synchronize ViewModel and View through weak reference, i.e. to implement the full MVVM pattern. This View will subscribe to ViewModel changes and change it's state. This principle also works in reverse. (Here, I lied, it has to work in a vacuum, but in a case of Android data-binding it isn't yet implemented. Read more in the article). It is particularly pleasing that Google will promote this approach and it eventually will become mainstream. Soon you will not get lost giving support to the project and figuring out what they've made. (Yeah, of course, all the Indians laugh at you). And we will surely abandon the newfangled MVP on Android.
In general, the data-binding concept is not a new one, C# developers have been long using it and it's related features, and it works great in the both sides. But better late than never. Farewell, findViewById, butterknife, Android annotations! Hello, concise coding!
I have already tested data-binding in one of my business projects. I found it very convenient, and in this article I would like to share the experience of it's correct and non-trivial usage.
Data-binding paradigm
Data-binding for each layout, which supports it, creates it's own child class using code generation, which will store all views of this layout that have an id. The developer has the ability to address them anytime after instance of class creating. This alone makes it possible to reduce significantly the number of rows in the class controller over view (Activity, Fragment, Adapter, etc) by getting rid of the fields with views and searching views through findViewById.
Let's see how the paradigm of working with the view using data-binding. After wrapping XML representation of the view in layout tag, view has the opportunity to request a ViewModel and update itself according to the model field:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable name="viewModel" type="com.example.ViewModel"/>
</data>
<TextVie android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.someFied}"
app:visibility="@{viewModel.isTextVisible?View.VISIBLE : View.INVISIBLE}"
/>
</layout>
Then the developer initializes data-binding and sets ViewModel to it.
Then the developer initializes data-binding and sets ViewModel to it.
It should be noted that if you use of tools-namespace to configure the preview, you need to declare it in the fields of the layout root tag. If you declare tools in wrapped view, the preview will not run.
After implementation of @Bindable annotated fields or Observable Fields of ViewModel that implements the IViewModel interface, view state will also vary with the fields of ViewModel class. Read more about it and other features of data-binding at android-developers.
It is rather interesting that the view reads information from ViewModel, subscribing to it's changes through the interface. ViewModel, thus, ideally should know nothing about the view, but again, this can not be achieved because of the absence of two-way binding. Thus, the principles of weak binding and interface-level programming are respected.
With automatic or custom setters you can set listeners to the view, specify any other parameters using @BindingAdapter. So far, unfortunately, we have to leave the dependence of ViewModel from the view in order to provide the ability to read the view changes. But if you call binding.textView1.setVisibility (View.VISIBLE) or similar methods, then there are chances that you are doing something wrong. Although, of course, in real applications you may need anything.
What is now possible
Using data-bindings it has become possible to apply previously unavailable features. Imagine that you need to implement the TextView, Button, Checkbox, and other text widgets with the ability to specify their custom font. You just do not want to call the method setTypeface for all such views in your fragment. It is desirable to specify the font in XML, as we do with the color of the font, orientation, and others. What is the standard approach in solving this problem? There will be created a style attribute with a list of possible custom fonts, a class that encapsulates the corresponding logic of one of the enums and the path to the font in the assets. And of course, classes inherited from TextView, Button and Checkbox, which will read the attributes of the class and call the class-controller of fonts. Disadvantages of this approach are obvious: we'll have to remake all the declares in XML of all views, there will be several classes that perform the same, but are inherited from the different widgets.
How I solved this problem in my project? I just created an adapter that performs this function:
public class TypefaceAdapter {
public enum Font {
PROXIMA_NOVA_REGULAR(R.attr.Proxima_Nova_Regular, "font/proximanova-regular.ttf"),
PROXIMA_NOVA_BLACK(R.attr.Proxima_Nova_Black, "font/proximanova-black.ttf"),
PROXIMA_NOVA_BOLD(R.attr.Proxima_Nova_Bold, "font/proximanova-bold.ttf");
private int value;
private String fontPath;
Font(int value, String fontPath){
this.value = value;
this.fontPath = fontPath;
}
public static Font getFontFromIndex(int idx){
for (Font font : values()){
if (font.value == idx){
return font;
}
}
return PROXIMA_NOVA_REGULAR;
}
}
@BindingAdapter("bind:font")
public static void setTypeface(TextView textView, int value){
Typeface myTypeface = cachedTypefaces.get(value);
if (myTypeface == null) {
Font font = Font.getFontFromIndex(value);
myTypeface = Typeface.createFromAsset(textView.getContext().getAssets(), font.fontPath);
cachedTypefaces.put(value, myTypeface);
}
textView.setTypeface(myTypeface);
}
}
Here is how the declaring of a target widget will look like:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:font="@{R.attr.Proxima_Nova_regular}" />
I should note that the establishment of the correspondence between the path to the font, which is in the assets, and the XML-file the identifier of R.attr is used. This is an empty attribute declared in resources.
<attr name="Proxima_Nova_Regular"/>
<attr name="Proxima_Nova_Black"/>
<attr name="Proxima_Nova_Bold"/>
And then in enum we used a simple equation to find a match, get the font filepath of a type found, and transfer into the method createFromAsset. It's simple. Unfortunately, the full support for data-binding in XML is not yet implemented, so far we would have to write the name of the attribute manually, without the autocompletion. As a result, I've created a class, which supports the principle of a single responsibility with the encapsulated in one place logic of the custom font setting. It is also remarkable that the Visual Studio recognizes the path to the font that I have passed to a row and I can quickly go to the file in the assets. This is also one of the recent features. Note that the Button, Checkbox, and others having setTypeface method are inherited from TextView. So they do not require further modification of the adapter. If the implemented method doesn't fit, create redefined methods.
Another case. Suppose we have a ViewPager with several fragments that work with the same data set, but display it differently. The user can change the state of one or more elements of this set while all other screens of the ViewPager should automatically support the change. Which came first? Fragments could download the data from the database on their own through the superclass, or to entrust this responsibility Activity. Once the item has been changed, we write the corresponding entity in the database, and all other fragments learned about the change via the loader, for example. Not very efficiently, isn't it? Especially if just one field has been changed. How can we improve it? Two things come to mind: Local Broadcast with ID of the modified model transfer, event through any event bus, observable pattern implementation of the model and signing all interested fragments on it. Well, it's better, though the list will still be completely updated each time the model is being updated. Of course, the view itself can be signed to the changes, but we'll receive a significant amount of code in the result.
Now let's compare it with a new feature: to start with, you create ViewModels in the Activity and distributed them to the fragments. (I recommend using feedback. Fragments subscribe to receive data from the Activity Activity doesn't distribute them). Then, if in case of data change you simply call setSomeField (..) in the ViewModel. If data-bindings is implemented, all binded views are updated automatically! No matter how many similar fragments exist and how much logic of binding of ViewModel to the view differs.
You can go ahead and develop the idea. In my project, it was necessary to perform the calculation of the sum of the model field in the list and output it. Moreover, the user could change the field in any of the models anytime. Accordingly, my ViewModel for the appropriate screen controlled not a single model, but a list of them (a list of child ViewModels). this ViewModel isn't able to find out that some field of the item in the list has changed. You need to listen to this change. What does it look like? The needed list of models is transmitted from somewhere to the method initCardList. Using addOnPropertyChangedCallback(..) method we sign the observer to follow the changes of all models status.
In onCardPropertyChangedObserver we check the modified field ID with ID, where you need to perform a recount. And update data if needed. setProficiencyLevel and getProficiencyLevel methods are responsible for the change and the presentation of the required information respectively. In child ViewModels binding should also be implemented through @BindingBindable or ObservableField. Everything is simple and pain-free. I like this code, and you?
public void initCardList(List<Card> cardList){
this.cardList = cardList;
for (Card card : cardList){
card.addOnPropertyChangedCallback(onCardPropertyChangedObserver);
}
notifyPropertyChanged(BR.totalCards);
setAnswerCard(cardList.get(0));
setQuestionCard(cardList.get(0));
updateAverageProficiencyLevel();
}
private OnPropertyChangedCallback onCardPropertyChangedObserver = new OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
if (propertyId == BR.proficiencyLevel){
updateAverageProficiencyLevel();
}
}
};
public void updateAverageProficiencyLevel(){
double sum = 0;
for (Card card: cardList) {
sum += card.getProficiencyLevel();
}
double res = sum/cardList.size();
setProficiencyLevel(res);
}
@Nullable @Bindable public String getProficiencyLevel() {
if (proficiencyLevel == null) return null; //round proficiency level to x.x value
DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(Locale.UK);
return new DecimalFormat("0.0", otherSymbols).format(proficiencyLevel);
}
private void setProficiencyLevel(Double proficiencyLevel) {
this.proficiencyLevel = proficiencyLevel;
notifyPropertyChanged(BR.proficiencyLevel);
}
What else to add? Despite the fact that it is possible to transfer a lot of code into XML, I do not recommend this because XML is not designed for it (even when the possibility autocompletion will be implemented). Believe me, you may have butthurt when there is a bug in the application because of the forgotten logic in an XML-file and you can not find it quickly. XML code should be as simple and short as possible. For everything else there's a viewModel and BindingAdapter.
Existing drawbacks, solutions and wishes
As I said before the interaction view and viewModel remains a big problem. In the current version of the binding, it has to be implemented manually. There are also problems with the full compliance with the MVVM pattern and some real issues. For example, if you decide to implement a checkbox with preset, you are likely to do something like that:
<CheckBox
android:id="@+id/checkBox_checkAll"
android:layout_below="@id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/check_all"
app:checked="@{resetVM.isCheckAllChecked}"
tools:checked="true"/>
private boolean isCheckAllChecked;
@Bindable
public boolean getIsCheckAllChecked() { return isCheckAllChecked; }
public void setIsCheckAllChecked(boolean isCheckAllChecked){
this.isCheckAllChecked = isCheckAllChecked;
notifyPropertyChanged(BR.isCheckAllChecked);
}
The problem is detected when you try to create an isCheckAllChecked field match with the current state of the checkbox, which is determined by the user:
private CompoundButton.OnCheckedChangeListener listener = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
setIsCheckAllChecked(true);
}
};
What if we set this listener to a checkbox and run the program? - StackOverflow. The fact is that the setIsCheckAllChecked method calls a field change event, then onCheckedChanged will be called again as well as a new event. Of course, in this case, you can make a private setter without property notifications, but then what to do if the checkbox controls the state of other checkboxes, and the others control it? Similarly. If you check this checkbox, then the controlled ones should be set, and if you uncheck reset. Correspondingly, if you have set all the checkboxes under control - isAllChecked checkbox should be checked and if, at least, one child checkbox has been reset - reset. Here we face with the cross looped logic. What is the solution? Implement an heir class from OnClickListener with the same onCheckedChange method signature and fulfill our condition in it.
public abstract class OnCheckedChangeClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (v instanceof CheckBox){
CheckBox cb = (CheckBox) v;
onCheckedChange(cb, cb.isChecked());
}
}
public abstract void onCheckedChange(CheckBox checkBox, boolean isChecked);
}
We set a specific implementation, the main checkbox:
binding.checkBoxCheckAll.setOnClickListener(new OnCheckedChangeClickListener() {
@Override
public void onCheckedChange(CheckBox checkBox, boolean isChecked) {
setIsCheckAllChecked(isChecked);
}
});
We set this listener to all controlled checkboxes (checkboxes array can be set in the constructor of the ViewModel):
private OnCheckedChangeClickListener checkedChangeListener = new OnCheckedChangeClickListener() {
@Override
public void onCheckedChange(CheckBox checkBox, boolean isChecked) {
boolean isAllChecked = true;
for (CheckBox childCheckBox : cba){
if (!childCheckBox.isChecked()){
isAllChecked = false;
break;
}
}
if (isAllChecked) {
binding.checkBoxCheckAll.setChecked(true);
} else {
binding.checkBoxCheckAll.setChecked(false);
}
}
};
XML general view:
<CheckBox
android:id="@+id/checkbox_reset_notes"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_below="@id/checkbox_reset_marks"
android:text="@string/reset_notes"
app:checked="@{resetVM.isCheckAllChecked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/checkbox_reset_proficiency_level"
app:checked="@{resetVM.isCheckAllChecked}"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_below="@id/checkbox_reset_notes"
android:text="@string/reset_proficiency_level_of_cards"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/checkbox_reset_reviews"
app:checked="@{resetVM.isCheckAllChecked}"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_below="@id/checkbox_reset_proficiency_level"
android:text="@string/reset_reviews"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
I should note that here I moved away from the standard, too, when I set the state to the main checkbox by clicking on the child one. Since if you call a setIsAllChesked method, the current implementation will lead us to the infinite loop (as the child checkboxes are also tied to isCheckAllChecked).
In order to simplify our lives, it would be enough for Google to implement a setChecked method (boolean isChecked, boolean isUserInteraction), which would not call OnCheckedChangeListener again at reseted second parameter. Unfortunately, Google is reluctant to make life easier for developers. (Remember the Material Design? We got some new design with guidelines, but all the standard widgets had to implement ourselves. Google provided something useful only a year after).
I also often face the fact that some views have, for example, the same left margin, but only on a specific current screen. How to encapsulate the value of the margin in one place, which may change in the future? So far, this is done through dimens resources. But in this case, these values are available for all applications at once (badly encapsulated). It would be great if in data tag it would be possible to declare variables and assign values to them so that only the current XML could know about them. I asked about this and, unfortunately, the team engaged in bindings is not planning to implement it in the near future.
So we look forward to two-way binding, autocomplete in XML and there is nothing more to hope.
Data-binding is a new official feature, which will soon take hold on many projects as it seriously simplifies and improves the workflow for any Android mobile app development company, introduces the possibility of using the MVVM pattern in Android, and extends the life of the developer. So far it is in the beta stage and binding support is expected to improve in the future.
Comments