12/08/2018, 09:43

MVP Pattern for Android

Mô hình MVP (Model View Presenter) là một dẫn xuất từ mô hình MVC (Model View Controller), hiện tại đang được áp dụng trong nhiều ứng dụng Android. Bài viết này giới thiệu khái quát về mô hình MVP cũng như đưa ra một trong số rất nhiều cách triển khai mô hình này trên Android. Mô hình MVP là ...

Mô hình MVP (Model View Presenter) là một dẫn xuất từ mô hình MVC (Model View Controller), hiện tại đang được áp dụng trong nhiều ứng dụng Android.

Bài viết này giới thiệu khái quát về mô hình MVP cũng như đưa ra một trong số rất nhiều cách triển khai mô hình này trên Android.

Mô hình MVP là gì?

MVP là một mô hình kiến trúc hướng giao diện người dùng, được thiết kế để tạo thuận lợi cho việc kiểm thử đơn vị (unit testing) và tăng tính tách biệt giữa tầng dữ liệu và tầng hiển thị dữ liệu trong mô hình MVC.

Mô hình MVP cho phép tách tầng trình diễn (Presenter) ra khỏi tầng dữ liệu (Model), vì vậy tương tác với giao diện được tách biệt với cách chúng ta biểu diễn nó trên màn hình (View), hay nói cách khác, tất cả logic khi người dùng tương tác được tách ra và đưa vào tầng trình diễn. Thiết kế lý tưởng nhất là với cùng một logic được áp dụng cho nhiều View khác nhau và hoán đổi được cho nhau.

Trong đó:

  • Model là một interface xác định cách mà dữ liệu được hiển thị trong giao diện người dùng.
  • View là một giao diện người dùng thụ động hiển thị dữ liệu (Model) và tiếp nhận tương tác người dùng và truyền tới để Presenter xử lý tương tác.
  • Presenter được ví như middle-man. Khi người dùng tương tác với View, Presenter tiếp nhận tương tác người dùng và update Model. Khi Model được update hay có thay đổi, Presenter lấy dữ liệu từ Model, định dạng và đưa tới View để hiển thị.

Model View Presenter

Luồng dữ liệu trong mô hình MVP

Điểm khác biệt dễ thấy nhất ở đây khi so sánh mô hình MVP với mô hình MVC chính là vị trí cũng như chức năng của tầng Presenter và View so với tầng Controller:

Trong mô hình MVP, tầng View là tầng duy nhất tiếp nhận tương tác người dùng thay vì cả 2 tầng View và tầng Controller trong mô hình MVC. Logic xử lý tương tác cũng như logic xử lý dữ liệu hiển thị được tách ra trong tầng Presenter thay vì được gộp chung với tiếp nhận tương tác trong Controller.

Tại sao sử dụng MVP?

Trong Android, có một vấn đề phát sinh từ thực tế là các cơ chế xử lý tương tác trong Android được kết hợp chặt chẽ giữa giao diện người dùng và xử lý, truy cập dữ liệu.

  • Một ví dụ điển hình là CursorAdapter, đây là sự kết hợp của việc định dạng dữ liệu với xử lý tương tác giao diện. Đôi khi trong đó còn bao gồm cả xử lý dữ liệu ở mức sâu hơn (như tương tác với CSDL) thông qua Cursor.

Việc kết hợp này có thể giúp giảm thiểu lượng code trong ứng dụng cũng như gộp các xử lý, logic trong ứng dụng vào cùng một nơi. Nhưng đối với một ứng dụng liên tục phát triển, hay một ứng dụng lớn, việc này khiến cho lượng code trên mỗi logic trở nên rất lớn, các logic xen lần, chồng chéo lên nhau, rất khó cho việc đọc hiểu, bảo trì cũng như mở rộng.

Phân tầng, phân lớp ứng dụng có thể giảm đi hiệu năng do ứng dụng cần thêm nhiều tài nguyên, nhưng tính khả chuyển của ứng dụng cũng tăng gấp nhiều lần., không những thế còn mở rộng khả năng cho phép kiểm thử từng phần của ứng dụng trở nên dễ dàng hơn. Chính vì thế cân nhắc giữa hiệu năng và tính dễ dàng mở rộng, bảo trì là hết sức quan trọng.

Trong Android, kiểm thử là một vấn đề khó khăn vì mối liên kết chặt chẽ giữa giao diện, logic và dữ liệu. MVP tách biệt giao diện khỏi dữ liệu, chia ứng dụng ra thành ít nhất ba lớp khác nhau, từ đó có thể kiểm thử một các độc lập. Với MVP ta có thể kiểm thử đo đạc một cách tối đa các logic của ứng dụng.

Triển khai MVP trên Android

Có rất nhiều biến thể cũng như phương pháp triển khai MVP, tất cả mọi người có thể điều chỉnh mô hình này tùy theo nhu cầu và cách họ cảm thấy thoải mái hơn. Các mô hình này, về cơ bản khác nhau ở số lượng chức năng mà tầng Presenter đảm nhận.

Một view nhận tương tách từ người dùng disable hoặc enable progress bar, liệu có nên giao nhiệm vụ này cho một presenter? Một Activity sẽ nhận sự kiện click vào nút settings trên ActionBar hay sự kiện này sẽ được một đối tượng presenter trong Activity đảm nhận?

Những câu hỏi như vậy tạo ra nhiều cách để chúng ta triển khai mô hình MVP, và trên thực tế chưa có một tiêu chuẩn chính xác nào được đưa ra. Vì vậy dưới đây tôi xin trình bày một trong những phương pháp đó.

Tầng trình diễn - Presenter

Tầng trình diễn có trách nhiệm như một middle-man giữa View và Model. Nó lấy dữ liệu từ Model, định dạng và trả về cho View. Nhưng không giống như MVC, nó cũng quyết định những gì sẽ xảy ra khi người dùng tương tác với View, hay nói cách khác nó hàm chứa logic ứng dụng.

Tầng logic dữ liệu - Model

Trong một ứng dụng với thiết kế kiến trúc tốt, mô hình này sẽ chỉ là một gateway giữa tầng domain và tầng business logic. Trong mô hình Clean Architecture của Uncle Bob, Model sẽ là một interactor thự thi một use case. Để đơn giản, ở đây Model đơn thuần được nhìn nhận như một data source - cung cấp dữ liệu chúng ta muốn hiển thị trong giao diện ứng dụng.

Tầng giao diện - View

View, thường được implement bởi một Activity (hoặc có thể là một Fragment, một View ... tùy thuộc vào cấu trúc ứng dụng), Activity này sẽ chứa một thuộc tính là một lớp Presenter. Lý tưởng nhất Presenter nên được cung cấp bởi một Dependency Injection framewok như Dagger, nhưng trong trường hợp ứng dụng không sử dụng một thư viện hay framework như vậy ta hoàn toàn có thể tạo ra các đối tượng Presenter này.

Sau đây là ví dụ triển khai một màn hình Login trong Android:

  • Lớp LoginActivity implements LoginView interface để hiển thị kết quả tới người dùng, đồng thời một thuộc tính LoginPresenter chịu trách nhiệm tiếp nhận và thực thi logic ứng dụng (ở đây là xác thực người dùng) và trả
public class LoginActivity extends Activity implements LoginView, View.OnClickListener {

    private ProgressBar progressBar;
    private EditText username;
    private EditText password;
    private LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        progressBar = (ProgressBar) findViewById(R.id.progress);
        username = (EditText) findViewById(R.id.username);
        password = (EditText) findViewById(R.id.password);
        findViewById(R.id.button).setOnClickListener(this);

        presenter = new LoginPresenterImpl(this);
    }

    @Override public void showProgress() {
        progressBar.setVisibility(View.VISIBLE);
    }

    @Override public void hideProgress() {
        progressBar.setVisibility(View.GONE);
    }

    @Override public void setUsernameError() {
        username.setError(getString(R.string.username_error));
    }

    @Override public void setPasswordError() {
        password.setError(getString(R.string.password_error));
    }

    @Override public void navigateToHome() {
        startActivity(new Intent(this, MainActivity.class));
        finish();
    }

    @Override public void onClick(View v) {
        presenter.validateCredentials(username.getText().toString(), password.getText().toString());
    }
}
  • LoginView là một lớp giao diện cho phép phương thức hiển thị có thể đuợc thực thi hay hoán đổi dễ dàng trên nhiều Activity khác nhau.
public interface LoginView {
    public void showProgress();

    public void hideProgress();

    public void setUsernameError();

    public void setPasswordError();

    public void navigateToHome();
}
  • Tương tự như LoginView, LoginPresenter cũng giúp cho việc triển khai nhiều logic trên cùng một Activity hay hoán đổi các logic này cho nhau thông qua việc khởi tạo một lớp Implement khác.
public interface LoginPresenter {
    public void validateCredentials(String username, String password);
}
  • Lớp OnLoginFinishedListener ở đây được sử dụng như một custom listener cho phép đa dạng hóa các phương thức hiển thị kết quả tới người dùng.
public interface OnLoginFinishedListener {

    public void onUsernameError();

    public void onPasswordError();

    public void onSuccess();
}
  • Giống như với OnLoginFinishedListener, LoginInteractor cũng cho phép custom các phương thức tương tác của người dùng với ứng dụng.
public interface LoginInteractor {
    public void login(String username, String password, OnLoginFinishedListener listener);
}
  • Và cuối cùng là các lớp implement, thực thi logic ứng dụng, phương thức hiển thị kết quả cũng như phương thức tiếp nhận tương tác người dùng.
public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener {

    private LoginView loginView;
    private LoginInteractor loginInteractor;

    public LoginPresenterImpl(LoginView loginView) {
        this.loginView = loginView;
        this.loginInteractor = new LoginInteractorImpl();
    }

    @Override public void validateCredentials(String username, String password) {
        loginView.showProgress();
        loginInteractor.login(username, password, this);
    }

    @Override public void onUsernameError() {
        loginView.setUsernameError();
        loginView.hideProgress();
    }

    @Override public void onPasswordError() {
        loginView.setPasswordError();
        loginView.hideProgress();
    }

    @Override public void onSuccess() {
        loginView.navigateToHome();
    }
}
public class LoginInteractorImpl implements LoginInteractor {

    @Override
    public void login(final String username, final String password, final OnLoginFinishedListener listener) {
        // Mock login. I'm creating a handler to delay the answer a couple of seconds
        new Handler().postDelayed(new Runnable() {
            @Override public void run() {
                boolean error = false;
                if (TextUtils.isEmpty(username)){
                    listener.onUsernameError();
                    error = true;
                }
                if (TextUtils.isEmpty(password)){
                    listener.onPasswordError();
                    error = true;
                }
                if (!error){
                    listener.onSuccess();
                }
            }
        }, 2000);
    }
}

Dễ thấy ở đây, việc thay đổi logic ứng dụng, phương thức hiển thị hay cách thức tương tác là hết sức dễ dàng bằng việc thêm các lớp Interface, Implement khác hay viết các lớp wrapper các lớp sẵn có. Điều này càng dễ dàng hơn trong Java 8 khi một dạng phương thức mới - phương thức default được áp dụng. Tôi sẽ nói tới dạng phương thức này trong một bài viết sớm nhất.

Reference

  1. Antonio Leiva Gordillo - MVP for Android: how to organize the presentation layer
  2. Uncle Bob - The Clean Architecture
  3. Wikipedia - Model–view–presenter pattern
  4. Fernando Cejas - Architecting Android…The clean way?
  5. Jeff Angellini - AN MVP PATTERN FOR ANDROID
  6. Konstantin Mikheev - Introduction to Model-View-Presenter on Android
0