Trong bài hướng dẫn này, chúng ta sẽ thảo luận và triển khai mô hình MVVM trong Android.
Tại sao chúng ta cần các pattern dạng này? Việc dồn tất cả logic vào một Activity hay Fragment duy nhất sẽ gây khó khăn cho việc kiểm thử (testing) và tái cấu trúc (refactoring) code. Do đó, chúng ta nên tách biệt code và áp dụng một kiến trúc sạch.
Android MVVM
MVVM là viết tắt của Model, View, ViewModel.
- Model: Chứa dữ liệu của ứng dụng. Model không giao tiếp trực tiếp với View. Thông thường, dữ liệu từ Model nên được cung cấp cho ViewModel thông qua các Observable (đối tượng quan sát được).
- View: Đại diện cho phần UI của ứng dụng, không chứa bất kỳ logic nghiệp vụ (application logic) nào. Công việc của View là theo dõi ViewModel.
- ViewModel: Đóng vai trò trung gian giữa Model và View. ViewModel chịu trách nhiệm chuyển đổi dữ liệu từ Model, cung cấp các luồng dữ liệu (data stream) cho View. Nó cũng sử dụng các hook (móc nối) hoặc callback (hàm gọi lại) để cập nhật View. ViewModel sẽ yêu cầu dữ liệu từ Model.
Sơ đồ dưới đây minh họa luồng hoạt động cốt lõi của MVVM.
Vậy MVVM khác MVP như thế nào?
- ViewModel thay thế Presenter ở layer (tầng) giữa.
- Presenter giữ tham chiếu đến View, còn ViewModel thì không.
- Presenter cập nhật View theo cách truyền thống (gọi phương thức).
- ViewModel gửi đi các luồng dữ liệu.
- Presenter và View có mối quan hệ 1-1.
- View và ViewModel có mối quan hệ 1-nhiều (một ViewModel có thể tương tác với nhiều View).
- ViewModel không biết View nào đang lắng nghe nó.
Có hai cách để triển khai MVVM trong Android:
- Data binding (liên kết dữ liệu)
- RXJava
Trong bài hướng dẫn này, chúng ta sẽ chỉ sử dụng data binding. Thư viện Data Binding được Google giới thiệu nhằm mục đích liên kết dữ liệu trực tiếp trong file layout XML.
Chúng ta sẽ tạo một ứng dụng ví dụ đơn giản với chức năng đăng nhập và yêu cầu người dùng nhập thông tin của họ. Qua đó, ta sẽ thấy cách ViewModel thông báo cho View hiển thị một thông báo toast mà không cần giữ tham chiếu đến View.
Làm thế nào để thông báo đến một class mà không cần giữ tham chiếu của nó? Ta có ba cách để thực hiện điều này:
- Sử dụng liên kết dữ liệu hai chiều
- Sử dụng dữ liệu sống (live data)
- Sử dụng RxJava
Liên kết dữ liệu hai chiều
Liên kết dữ liệu hai chiều (two-way data binding) là một kỹ thuật liên kết các đối tượng với layout XML, cho phép cả đối tượng và layout đều có thể gửi dữ liệu cho nhau. Trong trường hợp của chúng ta, ViewModel có thể gửi dữ liệu đến layout và đồng thời theo dõi các thay đổi từ layout.
Để làm được điều này, chúng ta cần một BindingAdapter
và một thuộc tính tùy chỉnh được định nghĩa trong XML. BindingAdapter
sẽ lắng nghe những thay đổi của thuộc tính này. Chúng ta sẽ tìm hiểu kỹ hơn về liên kết dữ liệu hai chiều qua ví dụ cụ thể dưới đây.
Cấu trúc dự án ví dụ Android MVVM
Thêm thư viện Data Binding
Thêm đoạn code sau vào file build.gradle
của ứng dụng:
android {
dataBinding {
enabled = true
}
}
Thao tác này sẽ kích hoạt Data Binding trong ứng dụng của bạn.
Thêm các dependency
Thêm các dependency (phụ thuộc) sau vào file build.gradle
của bạn:
implementation 'android.arch.lifecycle:extensions:1.1.0'
Model
Model sẽ chứa email và mật khẩu của người dùng. Cụ thể, class User.java
dưới đây sẽ làm điều đó:
package com.journaldev.androidmvvmbasics.model;
public class User {
private String email;
private String password;
public User(String email, String password) {
this.email = email;
this.password = password;
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
Liên kết dữ liệu hai chiều cho phép chúng ta liên kết các đối tượng trong layout XML theo cách mà cả đối tượng và layout đều có thể gửi dữ liệu cho nhau. Cú pháp của nó là @={variable}
.
Layout
Code cho activity_main.xml
sẽ giống như sau:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:bind="<https://schemas.android.com/tools>">
<data>
<variable
name="viewModel"
type="com.journaldev.androidmvvmbasics.viewmodels.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">
<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="@={viewModel.userEmail}" />
<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="@={viewModel.userPassword}" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:onClick="@{()-> viewModel.onLoginClicked()}"
android:text="LOGIN"
bind:toastMessage="@{viewModel.toastMessage}" />
</LinearLayout>
</ScrollView>
</layout>
Data Binding yêu cầu chúng ta đặt tag <layout>
ở đầu file. Trong ví dụ này, ViewModel sẽ liên kết dữ liệu với View. Lệnh gọi ()-> viewModel.onLoginClicked()
sẽ kích hoạt một lambda listener cho sự kiện click của Button, vốn được định nghĩa trong ViewModel. EditText
sẽ cập nhật các giá trị trong Model (thông qua ViewModel). Thuộc tính tùy chỉnh bind:toastMessage="@{viewModel.toastMessage}”
được sử dụng cho liên kết dữ liệu hai chiều. Dựa trên những thay đổi của toastMessage
trong ViewModel, BindingAdapter
tương ứng trong View sẽ được kích hoạt.
ViewModel
Đoạn code cho LoginViewModel.java
:
package com.journaldev.androidmvvmbasics.viewmodels;
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.text.TextUtils;
import android.util.Patterns;
import com.android.databinding.library.baseAdapters.BR;
import com.journaldev.androidmvvmbasics.model.User;
public class LoginViewModel extends BaseObservable {
private User user;
private String successMessage = "Login was successful";
private String errorMessage = "Email or Password not valid";
@Bindable
private String toastMessage = null;
public String getToastMessage() {
return toastMessage;
}
private void setToastMessage(String toastMessage) {
this.toastMessage = toastMessage;
notifyPropertyChanged(BR.toastMessage);
}
public void setUserEmail(String email) {
user.setEmail(email);
notifyPropertyChanged(BR.userEmail);
}
@Bindable
public String getUserEmail() {
return user.getEmail();
}
@Bindable
public String getUserPassword() {
return user.getPassword();
}
public void setUserPassword(String password) {
user.setPassword(password);
notifyPropertyChanged(BR.userPassword);
}
public LoginViewModel() {
user = new User("","");
}
public void onLoginClicked() {
if (isInputDataValid())
setToastMessage(successMessage);
else
setToastMessage(errorMessage);
}
public boolean isInputDataValid() {
return !TextUtils.isEmpty(getUserEmail()) && Patterns.EMAIL_ADDRESS.matcher(getUserEmail()).matches() && getUserPassword().length() > 5;
}
}
Các phương thức được gọi từ layout XML được triển khai trong ViewModel với cùng signature (khai báo hàm). Nếu phương thức tương ứng trong XML không tồn tại, chúng ta cần thay đổi thuộc tính thành app:
.
Class trên có thể kế thừa từ ViewModel
, nhưng ở đây chúng ta sử dụng BaseObservable
vì nó giúp chuyển đổi dữ liệu thành các luồng (stream) và tự động thông báo khi một thuộc tính toastMessage
thay đổi.
Chúng ta cần định nghĩa getter và setter (phương thức truy xuất và thay đổi) cho thuộc tính tùy chỉnh toastMessage
đã khai báo trong XML. Bên trong setter của toastMessage
, chúng ta sẽ thông báo cho observer (chính là View trong ứng dụng) rằng dữ liệu đã thay đổi. View (Activity của chúng ta) sau đó có thể xác định hành động xử lý cần thiết tương ứng.
Class BR
được Data Binding tự động sinh ra khi bạn rebuild lại project.
Hãy xem qua code cho class chứa BindingAdapter
:
package com.journaldev.androidmvvmbasics.views;
import android.databinding.BindingAdapter;
import android.databinding.DataBindingUtil;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import com.journaldev.androidmvvmbasics.R;
import com.journaldev.androidmvvmbasics.databinding.ActivityMainBinding;
import com.journaldev.androidmvvmbasics.viewmodels.LoginViewModel;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
activityMainBinding.setViewModel(new LoginViewModel());
activityMainBinding.executePendingBindings();
}
@BindingAdapter({"toastMessage"})
public static void runMe(View view, String message) {
if (message != null)
Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show();
}
}
Nhờ có Data Binding, class ActivityMainBinding được tự động sinh ra từ layout. Phương thức @BindingAdapter
sẽ được kích hoạt bất cứ khi nào giá trị của thuộc tính toastMessage
được định nghĩa Button thay đổi. Nó phải khớp với tên đã định nghĩa trong XML và được ViewModel sử dụng.
Như vậy, trong ứng dụng này, ViewModel cập nhật Model bằng cách lắng nghe các thay đổi từ View. Ngược lại, Model cũng có thể cập nhật View thông qua ViewModel với notifyPropertyChanged
.
Tương tác thực tế của ứng dụng sẽ trông giống như hình minh họa dưới đây.
Tổng kết
Bạn có thể tải mã nguồn ví dụ ở trên từ link này. Đừng ngần ngại thử nghiệm những kiến thức trên vào những project của riêng mình để trở nên thành thục hơn trong việc sử dụng kiến trúc MVVM trong Android nhé.