Architecture Pattern
Is a set of rules to explain what classes we have? How they will interact with each other to implement a particular system.
Common architecture patterns include:
Common architecture patterns in android like MVC, MVP, MVVM. And in this article, I would like to introduce how to implement MVVM with Data Binding Library.
MVVM with Data Binding
The role of each component in MVVM:
Model: Contains data of the application
View: Display data to the user
ViewModel: Contains business logic and provides data to display on View
Data Binding binds observable fields in ViewModel to UI elements like TextView or ImageView... through declarations and can perform bi-directionally synchronization between View and ViewModel.
Data Binding Library
You can use the Data Binding Library on devices running Android 4.0 (API level 14) or higher.
Build environment
To use the Data Binding Library, add dataBinding
to the build.gradle file in the app module as follows:
android {
...
dataBinding {
enabled = true
}
}
To speed up the build process and avoid some unnecessary errors. Add the following option to the gradle.properties file.
android.databinding.enableV2=true
Layouts và binding expressions
The layout file must start with the layout
tag followed by an element data
and the root view
. Here is an example:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
The user
variable in the data
tag describes the object that can be used in the view
.
Data Object
To use data objects in layout, you must declare it as a variable in the data
tag.
When you declare user.firstname
the library can access data to the firstname
field, the getFirstname()
method or the firstname()
method of the user
object passed in.
Data Binding
The library will automatically generate binding classes for each layout file whose root tag is layout. By default, the name of the class is based on the name of the layout file that converts to Pascal case and add Binding
to the suffix. For example:
Layout file | Binding class |
activity_main.xml | ActivityMainBinding.java |
fragment_main.xml | FragmentMainBinding.java |
item_user.xml | ItemUserBinding.java |
Following is an example to implement binding data:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
If you are using data binding in Fragment, ListView or RecyclerView adapter then you can use the following methods:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
Event handling
Data binding allows you to write expressions to handle events dispatched from the view. You can use the following to handle events:
Method references
The syntax is similar to the android:onClick
that we usually do when attaching to the method in the activity. However, with Data Binding it will check if the handler method is valid or not at compile time instead of running time. If there is an error it will return a compile time error.
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
Note: The signature of the method in the expression in the layout needs to be the same as the signature of the method in the listener object. Signature includes method name, parameter list and its type.
Listener bindings
When the view fires the event, the data binding will evaluate the binding expression. If the event you are listening for returns a non-void data type, the binding expression must also return the same data type. For example:
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
Imports, variables và includes
Import: allows you to easily refer to classes outside of your layout file from your binding expression. The following example will import the TextUtils
class to the layout file.
<data>
<import type="android.text.TextUtils"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"
android:visibility="@{TextUtils.isEmpty(user.username) ? View.GONE : View.VISIBLE, default=`gone`}"
Type alias: To avoid class name conflicts due to many classes with the same name in different packages, we can use alias. For example:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
Include: Can add child layouts. For example:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
Observable Object
An Observable is an object that can register/unsubscribe observers or notify observers of its own changes in the observer design pattern. Data Binding provides you with two ways to make an object, field or collection obserable:
Build-in observable class: Data binding provides the following built-in observable classes:
For example:
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:checked="@{vm.checkbox}"/>
public final ObservableBoolean checkbox = new ObservableBoolean();
Since the checkbox is an obserable object, it can automatically generate a notification to update the UI when there is any data change in it by calling checkbox.set(true or flase)
.
Implement Observable interface
The obserable interface allows us to add or remove listeners but we have to manually create change notifications to the listeners. And to make it easier we should implement the BaseObservable interface. By using the Bindable annotation to the getter and calling notifyPropertyChanged() on the setter method like the following example:
private static class User extends BaseObservable {
private String username;
@Bindable
public String getUserName() {
return this.username;
}
public void setUserName(String username) {
this.username = username;
notifyPropertyChanged(BR.username);
}
}
Data binding will generate a BR class containing the IDs of the resources used in the data binding. Each Bindable annotation will generate a corresponding ID in the BR class during compilation.
Binding adapters
Binding adapters are responsible for calling the setter method of the respective attribute to set a new value when the value of the attribute changes or to set an event listener.
Data binding will automatically call the default setter method or we can specify an alternative setter method or overwrite the default setter method.
For example, if we have an attribute of example
, the library will automatically look for the setExample(arg)
method to set a new value when the example
attribute changes.
Specify an alternative setter method
Some attributes have names that do not match the name of the setter method. Then we need to use BindingMethods to specify the alternative setter method. For example, the android:tint
attribute does not have a corresponding setter method, setTint, but it binds to the setter method setImageTintList
.
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
Custom logic
When you want to customize a binding logic to meet your business, for example, load an image from the internet and then display it on the view.
<ImageView
android:id="@+id/image_product"
android:layout_width="96dp"
android:layout_height="96dp"
app:image="@{product.image_url}"
bind:type="@{`product`}"/>
@BindingAdapter(value={"image", "type"}, requireAll=false)
public static void loadImage(ImageView view, String url, String type) {
ImageLoader.load(url).into(view);
}
Note: The binding adapter that you define will override the default binding adapter if a confict occurs.
Two-way data binding
It's two-way data binding. One way is when there is a state change in the UI, it will be updated to the data resource and vice versa if there is any change in the data resource, it will be updated to the UI.
To perform two-way data binding you need to add =
after @
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:checked="@={vm.checkbox}"/>
public final ObservableBoolean checkbox = new ObservableBoolean();
Suppose now that I want to perform a logic L when there is a change in data in object O, what will I do? Then we can do it by instantiating the OnPropertyChangedCallback listener and registering it on the O object. As in the following example, we implement the logic to enable the login button when the user has entered both username, password and checkbox.
public final ObservableField<String> username = new ObservableField<>();
public final ObservableField<String> password = new ObservableField<>();
public final ObservableBoolean checkbox = new ObservableBoolean();
public UserModelView(LoginNavigator navigator) {
Observable.OnPropertyChangedCallback callback = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
notifyPropertyChanged(BR.loginEnabled);
}
};
username.addOnPropertyChangedCallback(callback);
password.addOnPropertyChangedCallback(callback);
checkbox.addOnPropertyChangedCallback(callback);
}
@Bindable
public boolean getLoginEnabled() {
if (checkbox.get()
&& !TextUtils.isEmpty(username.get())
&& !TextUtils.isEmpty(password.get())) {
return true;
}
return false;
}
public void onLoginButtonClicked() {
boolean login = mUserRepository.login(new User(username.get(), password.get()));
if (login) {
if (mNavigator != null) {
mNavigator.gotoMain();
}
}
}
I have created a demo project here.
References
https://developer.android.com/topic/libraries/data-binding/
https://medium.com/mindorks/android-architecture-patterns-mv-c-p-vm-4594574eeaa1/
https://herbertograca.com/2017/07/28/architectural-styles-vs-architectural-patterns-vs-design-patterns/