Tìm hiểu về data binding library
MVVM
4
databinding
1
White

bqthanh viết ngày 29/12/2018

Architecture Pattern

Là một tập hợp các quy tắc để giải thích chúng ta có những class nào? chúng sẽ tương tác với nhau ra sao để thực hiện một hệ thống cụ thể.

Các architecture pattern phổ biến như:

  • Domain Driven Design
  • Three-tier
  • Micro-kernel
  • Model-View-Controller
  • Model-View-ViewModel

Những architecture pattern phổ biến ở android như MVC, MVP, MVVM. Và ở bài viết này mình xin giới thiệu về cách thực hiện MVVM với Data Binding Library.

MVVM với Data Binding

Vai trò của từng thành phần trong MVVM:

  • Model: Chứa data của ứng dụng
  • View: Hiển thị data đến người dùng
  • ViewModel: Chứa các business logic và cung cấp data để hiển thị lên View.

Data Binding thực hiện bind / gắn kết các field observable ở ViewModel tới các UI element như TextView hay ImageView... thông qua các khai báo và có thể thực hiện đồng bộ bi-directionally / 2 chiều giữa View và ViewModel.

Data Binding Library

Bạn có thể sử dụng Data Binding Library trên các thiết bị chạy Android 4.0 (API level 14) hoặc cao hơn.

Bắt đầu

Build environment

Để sử dụng Data Binding Library hãy thêm dataBinding tới file build.gradle ở module app như sau:

android {
    ...
    dataBinding {
        enabled = true
    }
}

Để tăng tốc build process và tránh một số lỗi không cần thiết. Hãy thêm option sau đây tới file gradle.properties.

android.databinding.enableV2=true

Layouts và binding expressions

File layout bắt buộc phải bắt đầu với thẻ layout và theo sau nó là một element data và root view. Sau đây là một ví dụ:

<?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>

Biến user trong thẻ data mô tả đối tượng có thể được sử dụng trong view.

Data Object

Để sử dụng các đối tượng dữ liệu trong layout bắt buộc bạn phải khai báo nó giống như một variable trong thẻ data.

Khi bạn khai báo user.firstname thư viện có thể truy xuất dữ liệu tới field firstname, method getFirstname() hay method firstname() của đối tượng user truyền vào.

Binding data

Thư viện sẽ tự động sinh các binding class cho mỗi file layout mà có root tag là layout. Mặc định thì tên của class dựa vào tên của file layout convert tới Pascal case và thêm Binding vào suffix / hậu tố. Ví dụ:

Layout file Binding class
activity_main.xml ActivityMainBinding.java
fragment_main.xml FragmentMainBinding.java
item_user.xml ItemUserBinding.java

Sau đây là ví dụ để thực hiện 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);
}

Nếu bạn đang sử dụng data binding trong Fragment, ListView hay RecyclerView adapter thì có thể sử dụng các method sau:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// hoặc
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

Event handling

Data binding cho phép bạn viết biểu thức xử lý event được dispatch / gửi từ view. Bạn có thể sử dụng các cơ chế sau đây để xử lý sự kiện:

Method references

Cú pháp thì tương tự android:onClick mà chúng ta hay làm khi gắn với method trong activity. Tuy nhiên với Data Binding thì nó sẽ kiểm tra xem method handler có hợp lệ hay không ngay tại compile time thay vì runnning time. Nếu có lỗi nó sẽ trả về một 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}"/>

Lưu ý: Signature / chữ kí của method trong biểu thức ở layout cần phải giống với signature của method trong đối tượng listener.

Signature: Bao gồm tên method, danh sách parameter và type của nó.

Listener bindings

Khi view bắn ra sự kiện thì data binding sẽ thực hiện đánh giá biểu thức binding. Nếu sự kiện bạn đang lắng nghe trả về kiểu dữ liệu không phải void thì biểu thức binding cũng phải trả về cùng kiểu dữ liệu đó. Ví dụ:

public class Presenter {
    public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
Imports, variables và includes

Import: cho phép bạn dễ dàng tham chiếu tới class bên ngoài file layout từ binding expression của bạn. Ví dụ sau đây sẽ import class TextUtils tới 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: Để tránh xung đột class name do nhiều class trùng tên khác package chúng ta có thể sử dụng alias. Ví dụ:

<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>

Includes: Biến có thể được truyền vào các included layout. Ví dụ:

<?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>

Đối tượng observable

Observable là một đối tượng có thể thực hiện đăng kí / huỷ bỏ đăng kí các observer / đối tượng lắng nghe hay notify / thông báo tới các observer về những thay đổi của chính nó trong observer design pattern. Data Binding cung cấp cho bạn 2 cách để làm đối tượng, field hay collection là obserable đó là:

  • Sử dụng build-in observable class
  • Xây dựng các đối tượng obserable bởi implement interface Observable

Build-in observable class

Data binding cung cấp sẵn các build-in obserable class như sau:

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable
  • ObservableArrayMap

Ví dụ:

<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();

Do checkbox là đối tượng obserable nên nó có thể tự động tạo ra một thông báo để update UI khi có bất cứ thay đổi data nào ở nó bởi thực hiện gọi checbox.set(true or flase).

Implement interface Observable

Interface obserable cho phép chúng ta add hay remove các listener nhưng chúng ta phải tự tạo các thông báo thay đổi tới các listener. Và để dễ dàng hơn chúng ta nên implement interface BaseObservable. Bằng cách sử dụng annotation Bindable tới getter và gọi notifyPropertyChanged() ở method setter giống như ví dụ sau đây:

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 sẽ sinh một class là BR chứa ID của các resource được sử dụng trong data binding. Mỗi annotation Bindable sẽ sinh một ID tương ứng ở BR class trong suốt quá trình compilation / dịch.

Binding adapters

Binding adapters chịu trách nhiệm thực hiện gọi method setter của attribute tương ứng để set new value khi giá trị của thuộc tính thay đổi hoặc để set một event listener.

Data binding sẽ tự động gọi method setter mặc định hoặc chúng ta có thể chỉ định một method setter thay thế hay thực hiện overwrite method setter mặc định.

Ví dụ ta có attribute là example thì thư viện sẽ tự động sẽ tìm kiếm method setExample(arg) để thực hiện set new value khi attribute example thay đổi.

Chỉ định một method setter thay thế

Một vài attribute có tên không match với tên của method setter. Khi đó chúng ta cần sử dụng BindingMethods để chỉ định method setter thay thế. Ví dụ attribute android:tint không có method setter tương ứng là setTint nhưng nó liên kết với method setter là setImageTintList.

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])
Custom logic

Khi bạn muốn custom một binding logic để đáp ứng nghiệp vụ của mình ví dụ như thực hiện load một image từ internet sau đó hiển thị lên 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);
}

Lưu ý: Binding adapter mà bạn định nghĩa sẽ ghi đề default binding adapter nếu có confict xảy ra.

Two-way data binding

Đó là binding data 2 chiều. Một chiều là khi có thay đổi trạng thái ở UI sẽ được update vào data resource và ngược lại nếu có bất kì thay đổi nào ở data resource sẽ được update lên UI.

Để thực hiện two-way data binding bạn cần thực hiện thêm = vào sau @

<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();

Giả sử bây giờ mình muốn thực hiện một logic L khi có thay đổi data ở object O thì sẽ làm thế nào? Khi đó chúng ta có thể thực hiện bởi khởi tạo listener OnPropertyChangedCallback và đăng kí nó vào object O. Như ví dụ sau mình thực hiện logic enable button login khi user đã nhập đủ ở cả username, password và 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();
            }
        }
    }

Ngoài ra mình có tạo sẵn một project demo tại đây.

Tài liệu tham khảo

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/

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

bqthanh

5 bài viết.
7 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
20 0
Tổng quan về AWS Amazon web services là một nền tảng điện toán đám mây được phát triển và cung cấp bởi Amazon. Regions and Availability Zones C...
bqthanh viết 1 năm trước
20 0
White
7 1
Tổng quan ORM hay Object Relational Mapping: Là một kĩ thuật cho phép bạn truy vấn và thao tác dữ liệu trên database bằng cách sử dụng mô hình hướ...
bqthanh viết 6 tháng trước
7 1
White
6 1
Tổng quan DNS Domain Name System hay hệ thống tên miền là một cơ sở dữ liệu phân tán nằm trên các server khác nhau lưu thông tin ánh xạ giữa domai...
bqthanh viết 8 tháng trước
6 1
Bài viết liên quan
White
0 0
Introduction to threeways binding with HtmlJs What is HtmlJs? It is a library/framework that implements MVVM pattern aka twoways binding in Java...
Nhan Nguyen viết 3 năm trước
0 0
White
5 0
Build your own rendering engine We’re not going to reinvent the wheel. We’re going to do simple things that help you have deep understanding of ho...
Nhan Nguyen viết hơn 2 năm trước
5 0
White
2 0
Hôm nay mình sẽ giới thiệu kiến trúc thiết kế web theo OOP, kiến trúc này mình đang dùng trong dự án ở trong công ty. Với kiến trúc này, bạn có thể...
Nhan Nguyen viết 3 năm trước
2 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
5 bài viết.
7 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!