Chúng ta đã triển khai mô hình MVVM sử dụng Data Binding và đã tìm hiểu riêng về LiveData cũng như Data Binding trong các bài hướng dẫn trước.
Hôm nay, chúng ta sẽ kết hợp LiveData với Data Binding trong ứng dụng Android sử dụng mô hình MVVM. Qua đó, chúng ta sẽ thấy cách LiveData hỗ trợ cập nhật giao diện người dùng từ ViewModel trở nên dễ dàng hơn như thế nào.
MVVM LiveData Data Binding
Trước đây, chúng ta đã dùng Data Binding để cập nhật View từ ViewModel. LiveData là một lớp tiện ích hoạt động như một vùng chứa dữ liệu cần truyền đi. Điểm nổi bật nhất của LiveData là khả năng nhận biết vòng đời (lifecycle-aware), nghĩa là nếu ứng dụng đang ở chế độ nền, giao diện sẽ không cố gắng cập nhật, từ đó tránh được nhiều lỗi thường gặp khi chạy ứng dụng.
Trong hướng dẫn này, chúng ta sẽ sử dụng MutableLiveData, vì lớp này cung cấp các phương thức công khai setValue()
và getValue()
.
Chúng ta sẽ tạo một ứng dụng đăng nhập đơn giản nhằm áp dụng các khái niệm vừa trình bày. Trước tiên, ta sẽ kết hợp LiveData với cơ chế Data Binding hai chiều, sau đó tiến hành refactor phần đang sử dụng Observable trong Data Binding để chuyển hoàn toàn sang LiveData.
Bắt đầu
Thêm các phụ thuộc sau vào tập tin build.gradle
của ứng dụng:
android {
...
dataBinding {
enabled = true
}
...
}
dependencies {
...
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'com.android.support:design:28.0.0-beta01'
...
}
Cấu trúc dự án
Tập tin LoginViewModelOld
sẽ chứa đoạn mã cũ, trong khi LoginViewModel
sẽ chứa đoạn mã đã được refactor.
Model
Chúng ta định nghĩa lớp Model trong tập tin User.java
như sau:
package com.journaldev.androidmvvmdatabindinglivedata;
import android.util.Patterns;
public class User {
private String mEmail;
private String mPassword;
public User(String email, String password) {
mEmail = email;
mPassword = password;
}
public String getEmail() {
if (mEmail == null) {
return "";
}
return mEmail;
}
public String getPassword() {
if (mPassword == null) {
return "";
}
return mPassword;
}
public boolean isEmailValid() {
return Patterns.EMAIL_ADDRESS.matcher(getEmail()).matches();
}
public boolean isPasswordLengthGreaterThan5() {
return getPassword().length() > 5;
}
}
Layout
Mã giao diện cho tập tin activity_main.xml
như sau:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:app="<https://schemas.android.com/apk/res-auto>">
<data>
<variable
name="loginViewModel"
type="com.journaldev.androidmvvmdatabindinglivedata.LoginViewModel" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:error="@{loginViewModel.errorEmail}"
app:errorEnabled="true">
<EditText
android:id="@+id/inEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress"
android:padding="8dp"
android:text="@={loginViewModel.email}" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:error="@{loginViewModel.errorPassword}"
app:errorEnabled="true">
<EditText
android:id="@+id/inPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword"
android:padding="8dp"
android:text="@={loginViewModel.password}" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:onClick="@{()-> loginViewModel.onLoginClicked()}"
android:text="LOGIN" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="@{loginViewModel.busy}" />
</LinearLayout>
</ScrollView>
</layout>
Thanh ProgressBar
sẽ được hiển thị để mô phỏng tính năng đăng nhập.
ViewModel
Mã nguồn của lớp LoginViewModel.java
được trình bày như sau:
package com.journaldev.androidmvvmdatabindinglivedata;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.ObservableField;
import android.os.Handler;
import android.support.annotation.NonNull;
public class LoginViewModel extends BaseObservable {
private String email;
private String password;
private int busy = 8;
public final ObservableField<String> errorPassword = new ObservableField<>();
public final ObservableField<String> errorEmail = new ObservableField<>();
public LoginViewModel() {
}
private MutableLiveData<User> userMutableLiveData;
LiveData<User> getUser() {
if (userMutableLiveData == null) {
userMutableLiveData = new MutableLiveData<>();
}
return userMutableLiveData;
}
@Bindable
@NonNull
public String getEmail() {
return this.email;
}
public void setEmail(@NonNull String email) {
this.email = email;
notifyPropertyChanged(BR.email);
}
@Bindable
@NonNull
public String getPassword() {
return this.password;
}
public void setPassword(@NonNull String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
@Bindable
public int getBusy() {
return this.busy;
}
public void setBusy(int busy) {
this.busy = busy;
notifyPropertyChanged(BR.busy);
}
public void onLoginClicked() {
setBusy(0); // View.VISIBLE
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
User user = new User(getEmail(), getPassword());
if (!user.isEmailValid()) {
errorEmail.set("Enter a valid email address");
} else {
errorEmail.set(null);
}
if (!user.isPasswordLengthGreaterThan5()) {
errorPassword.set("Password Length should be greater than 5");
} else {
errorPassword.set(null);
}
userMutableLiveData.setValue(user);
setBusy(8); // View.GONE
}
}, 5000);
}
}
ObservableField
là một lớp bao bọc hỗ trợ khả năng quan sát các thay đổi của trường dữ liệu. Trong ví dụ trên, đối tượng User
được quản lý thông qua LiveData
, cho phép theo dõi MainActivity
mỗi khi đối tượng User
thay đổi để kích hoạt các hành vi tương ứng.
Khi người dùng nhấn nút, ProgressBar
sẽ được hiển thị (View.VISIBLE = 0
, View.GONE = 8
). Sau khoảng trễ 5 giây, hệ thống tiến hành kiểm tra giá trị của email và mật khẩu, đồng thời cập nhật các thuộc tính liên kết (bindable
) trong TextInputLayout
.
Lưu ý, ObservableField
không nhận biết vòng đời.
Lớp MainActivity.java
được trình bày như sau:
package com.journaldev.androidmvvmdatabindinglivedata;
import android.arch.lifecycle.Observer;
import android.databinding.DataBindingUtil;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
import com.journaldev.androidmvvmdatabindinglivedata.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
LoginViewModel loginViewModel = new LoginViewModel();
binding.setLoginViewModel(loginViewModel);
loginViewModel.getUser().observe(this, new Observer<User>() {
@Override
public void onChanged(@Nullable User user) {
if (user.getEmail().length() > 0 || user.getPassword().length() > 0)
Toast.makeText(getApplicationContext(), "email : " + user.getEmail() + " password " + user.getPassword(), Toast.LENGTH_SHORT).show();
}
});
}
}
Trong đoạn mã trên, phương thức observe
theo dõi mọi thay đổi của đối tượng User
được chứa trong MutableLiveData
. Khi có thay đổi, một thông báo Toast
sẽ được hiển thị kèm theo tên đăng nhập và mật khẩu. Giờ đây, chúng ta sẽ thay thế hoàn toàn ObservableField
bằng LiveData
.
Refactor từ ObservableField sang LiveData
Mã nguồn mới cho lớp LoginViewModel.java
như sau:
package com.journaldev.androidmvvmdatabindinglivedata;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import android.os.Handler;
public class LoginViewModel extends ViewModel {
public MutableLiveData<String> errorPassword = new MutableLiveData<>();
public MutableLiveData<String> errorEmail = new MutableLiveData<>();
public MutableLiveData<String> email = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
public MutableLiveData<Integer> busy;
public MutableLiveData<Integer> getBusy() {
if (busy == null) {
busy = new MutableLiveData<>();
busy.setValue(8);
}
return busy;
}
public LoginViewModel() {
}
private MutableLiveData<User> userMutableLiveData;
LiveData<User> getUser() {
if (userMutableLiveData == null) {
userMutableLiveData = new MutableLiveData<>();
}
return userMutableLiveData;
}
public void onLoginClicked() {
getBusy().setValue(0); // View.VISIBLE
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
User user = new User(email.getValue(), password.getValue());
if (!user.isEmailValid()) {
errorEmail.setValue("Enter a valid email address");
} else {
errorEmail.setValue(null);
}
if (!user.isPasswordLengthGreaterThan5()) {
errorPassword.setValue("Password Length should be greater than 5");
} else {
errorPassword.setValue(null);
}
userMutableLiveData.setValue(user);
busy.setValue(8); // View.GONE
}
}, 3000);
}
}
Lớp trên hiện tại kế thừa từ ViewModel
do không còn cần sử dụng BaseObservable
. Các ObservableField
đã được thay thế bằng MutableLiveData
. Mọi thay đổi của MutableLiveData
sẽ được tự động cập nhật trong giao diện nhờ vào Data Binding.
Lớp MainActivity.java
sau khi cập nhật như sau:
package com.journaldev.androidmvvmdatabindinglivedata;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.databinding.DataBindingUtil;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
import com.journaldev.androidmvvmdatabindinglivedata.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
LoginViewModel loginViewModel = ViewModelProviders.of(this).get(LoginViewModel.class);
binding.setLoginViewModel(loginViewModel);
binding.setLifecycleOwner(this);
loginViewModel.getUser().observe(this, new Observer<User>() {
@Override
public void onChanged(@Nullable User user) {
if (user.getEmail().length() > 0 || user.getPassword().length() > 0)
Toast.makeText(getApplicationContext(), "email : " + user.getEmail() + " password " + user.getPassword(), Toast.LENGTH_SHORT).show();
}
});
}
}
ViewModelProviders.of
cũng có thể được sử dụng để khởi tạo một thể hiện ViewModel
như đã làm ở trên. Phương thức này chỉ khởi tạo ViewModel
một lần duy nhất, các lần gọi tiếp theo sẽ tái sử dụng thể hiện đã có. LifecycleOwner
là một interface mà Activity có thể liên kết.
Kết quả đầu ra của ứng dụng khi chạy được trình bày dưới đây:
Kết quả hiển thị của ứng dụng trong thực tế cho thấy thông báo Toast
sẽ không xuất hiện nếu người dùng rời khỏi ứng dụng, bởi vì LiveData nhận biết được vòng đời. Khi mở lại ứng dụng, thông báo sẽ hiển thị. Đây cũng là phần kết của bài hướng dẫn này.
Bạn có thể tải mã nguồn dự án từ liên kết bên dưới: