+2

MVC, MVP và MVVM trong Android

Phương pháp thực hành tốt nhất để tổ chức các ứng dụng Android vào các thành phần logic đã phát triển trong vài năm qua. Các cộng đồng đã phần lớn đã chuyển đi từ monolithic Model MVC pattern sang các pattern mô đun hóa và testable hơn.

Model View Presenter (MVP) & Model View ViewModel (MVVM) là hai trong số các lựa chọn thay thế có ứng dụng rộng rãi nhất. Để hiểu hơn cách hoạt động của các pattern, chúng ta sẽ dùng một game đơn giản là Tic-tac-toe.

  1. MVC

Các thành phần model, view, controller sẽ chia ứng dụng của bạn từ vĩ mô ra thành 3 set của trách nhiệm.

1.1 Model

Mô hình bao gồm Data + State + Business logic.

1.2 View

View là đại diện của các Model. View có trách nhiệm render cho giao diện người dùng (UI) và giao tiếp với controller khi người dùng tương tác với các ứng dụng. Trong kiến trúc MVC, View thường khá "dumb" trong đó nó không biết về các mô hình cơ bản và không biết về các state hoặc làm gì khi người dùng tương tác bằng cách nhấn một nút, typing một giá trị, vv

1.3 Controllder

Controller là Glue mà tie ứng dụng với nhau. Nó là điều khiển tổng thể cho những gì xảy ra trong ứng dụng. Khi View nói với controller rằng người dùng nhấp vào một nút, controller quyết định làm thế nào để tương tác với các Model phù hợp. Dựa trên dữ liệu thay đổi trong Model, controller có thể quyết định để cập nhật trạng thái của điểm phù hợp. Trong trường hợp của một ứng dụng Android, controller hầu như luôn luôn đại diện bởi một Activity hoặc Fragment.

Đây là cách nhìn nhận ở high level trong game Tic Tac Toe và các class đại diện mỗi phần:

Ví dụ controller:

public class TicTacToeActivity extends AppCompatActivity {

    private Board model;

    /* View Components referenced by the controller */
    private ViewGroup buttonGrid;
    private View winnerPlayerViewGroup;
    private TextView winnerPlayerLabel;

    /**
     * In onCreate of the Activity we lookup & retain references to view components
     * and instantiate the model.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tictactoe);
        winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
        winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
        buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);

        model = new Board();
    }

    /**
     * Here we inflate and attach our reset button in the menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_tictactoe, menu);
        return true;
    }
    /**
     *  We tie the reset() action to the reset tap event.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_reset:
                reset();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     *  When the view tells us a cell is clicked in the tic tac toe board,
     *  this method will fire. We update the model and then interrogate it's state
     *  to decide how to proceed.  If X or O won with this move, update the view
     *  to display this and otherwise mark the cell that was clicked.
     */
    public void onCellClicked(View v) {

        Button button = (Button) v;

        int row = Integer.valueOf(tag.substring(0,1));
        int col = Integer.valueOf(tag.substring(1,2));

        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            button.setText(playerThatMoved.toString());
            if (model.getWinner() != null) {
                winnerPlayerLabel.setText(playerThatMoved.toString());
                winnerPlayerViewGroup.setVisibility(View.VISIBLE);
            }
        }

    }

    /**
     * On reset, we clear the winner label and hide it, then clear out each button.
     * We also tell the model to reset (restart) it's state.
     */
    private void reset() {
        winnerPlayerViewGroup.setVisibility(View.GONE);
        winnerPlayerLabel.setText("");

        model.restart();

        for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
            ((Button) buttonGrid.getChildAt(i)).setText("");
        }
    }
}

1.4 Evalutaion

MVC làm một việc tuyệt vời của việc tách Model và View. Chắc chắn các Model có thể dễ dàng kiểm tra bởi vì nó không gắn với bất cứ thứ gì và View không có gì nhiều để test ở mức độ unit test.Tuy nhiên Controller có một vài vấn đề.

1.5 Các vấn đề của Controller

  • Testability : controller được gắn rất chặt chẽ đến các API Android mà rất khó để unit test.
  • Modularity & Flexibility : Các controller được kết chặt chẽ với cácView. Nó cũng có thể là một phần mở rộng của View. Nếu chúng ta thay đổi View, chúng ta phải quay trở lại và thay đổi Controller.
  • Maintenance : Theo thời gian, đặc biệt là trong các ứng dụng với mô hình anemic, càng ngày code của controller càng nhiều, làm cho chúng cồng kềnh và dễ gãy.

Và làm sao để sửa chúng? MVP là cứu tinh!

  1. MVP

MVP phá vỡ các Controller thành view / activity mà không buộc nó với phần còn lại của các responsibility "controller". Chi tiết hơn, nhưng chúng ta hãy bắt đầu lại với một định nghĩa chung về responsibility so với MVC.

2.1 Model

Tương tự MVC.

2.2 View

Sự thay đổi duy nhất ở đây là các Activity / Fragment bây giờ được coi là một phần của View. Chúng ta dừng lại việc chiến đấu với xu hướng tự nhiên để họ cùng đì. Good practice là có Activity thực hiện một giao diện interface sao cho presenter có một giao diện để code. Điều này loại bỏ khớp nối nó vào bất kỳ điểm cụ thể và cho phép unit test đơn giản với một implement mô hình của view.

2.3 Presenter

Đây thực chất là controller từ MVC ngoại trừ việc nó không gắn liền với View, mà chỉ là một interface. Việc này nêu lên những quan ngại khả năng kiểm thử cũng như các mối quan tâm của modularity/flexibility, chúng ta đã có với MVC. Trong thực tế, MVP chủ nghĩa thuần túy sẽ tranh luận rằng presenter không bao giờ nên có bất kỳ reference cho bất kỳ API Android hoặc code.

Nhìn vào Presenter chi tiết dưới đây, điều đầu tiên bạn sẽ nhận thấy là cách đơn giản hơn nhiều và rõ ràng hơn trong intent của mỗi action. Thay vì nói với view làm thế nào để hiển thị một cái gì đó, nó chỉ nói cho view những gì để hiển thị.

public class TicTacToePresenter implements Presenter {

    private TicTacToeView view;
    private Board model;

    public TicTacToePresenter(TicTacToeView view) {
        this.view = view;
        this.model = new Board();
    }

    // Here we implement delegate methods for the standard Android Activity Lifecycle.
    // These methods are defined in the Presenter interface that we are implementing.
    public void onCreate() { model = new Board(); }
    public void onPause() { }
    public void onResume() { }
    public void onDestroy() { }

    /** 
     * When the user selects a cell, our presenter only hears about
     * what was (row, col) pressed, it's up to the view now to determine that from
     * the Button that was pressed.
     */
    public void onButtonSelected(int row, int col) {
        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            view.setButtonText(row, col, playerThatMoved.toString());

            if (model.getWinner() != null) {
                view.showWinner(playerThatMoved.toString());
            }
        }
    }

    /**
     *  When we need to reset, we just dictate what to do.
     */
    public void onResetSelected() {
        view.clearWinnerDisplay();
        view.clearButtons();
        model.restart();
    }
}

Để làm công việc này mà không buộc activity vào presenter, chúng ta tạo ra một interface mà các Activity implement. Trong một test, chúng ta sẽ tạo ra một mô hình dựa trên interface này để kiểm tra sự tương tác với view từ những presenter.

2,4 Evaluation

Đã clean hơn rất nhiều. Chúng ta có thể dễ dàng unit test logic của presenter bởi vì nó không gắn với bất kỳ view cụ thể và Android API và đó cũng cho phép chúng ta làm việc với bất kỳ view khác miễn là view implement interface TicTacToeView.

2.5 Presenter concern

  • Maintenance - Presenters, giống như Controller, dễ bị sprinkled in, over time. Tại một số điểm, các developer thường xuyên tìm thấy chính mình với các presenter khó sử dụng lớn mà khó có thể phá vỡ.

Tất nhiên, các Developer cẩn thận có thể giúp ngăn chặn điều này, bởi siêng năng bảo vệ chống lại sự cám dỗ này là thay đổi ứng dụng theo thời gian. Tuy nhiên, MVVM có thể giúp giải quyết điều này bằng cách làm ít để bắt đầu. 3. MVVM

MVVM với Data Binding trên Android có lợi ích của việc kiểm tra dễ dàng hơn và mô đun, trong khi cũng làm giảm số lượng mã glue mà chúng ta phải viết để kết nối các view + model.

Hãy kiểm tra các phần của MVVM.

3.1 Model

Giống MVC

3.2 View

View bind tới các biến observable và hành động tiếp xúc bởi các ViewModel một cách linh hoạt.

3.3 ViewModel

ViewModel là chịu trách nhiệm cho model và chuẩn bị dữ liệu observable cần thiết cho View. Nó cũng cung cấp móc cho view để pass các event tới model. Tuy nhiên ViewModel không gắn với view.

Hãy cùng xem code cụ thể:

public class TicTacToeViewModel implements ViewModel {

    private Board model;

    /* 
     * These are observable variables that the viewModel will update as appropriate
     * The view components are bound directly to these objects and react to changes
     * immediately, without the ViewModel needing to tell it to do so. They don't
     * have to be public, they could be private with a public getter method too.
     */
    public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
    public final ObservableField<String> winner = new ObservableField<>();

    public TicTacToeViewModel() {
        model = new Board();
    }

    // As with presenter, we implement standard lifecycle methods from the view
    // in case we need to do anything with our model during those events.
    public void onCreate() { }
    public void onPause() { }
    public void onResume() { }
    public void onDestroy() { }

    /**
     * An Action, callable by the view.  This action will pass a message to the model
     * for the cell clicked and then update the observable fields with the current
     * model state.
     */
    public void onClickedCellAt(int row, int col) {
        Player playerThatMoved = model.mark(row, col);
        cells.put("" + row + col, playerThatMoved == null ? 
                                                     null : playerThatMoved.toString());
        winner.set(model.getWinner() == null ? null : model.getWinner().toString());
    }

    /**
     * An Action, callable by the view.  This action will pass a message to the model
     * to restart and then clear the observable data in this ViewModel.
     */
    public void onResetSelected() {
        model.restart();
        winner.set(null);
        cells.clear();
    }

}

Một vài trích đoạn từ chế độ view để xem làm thế nào các biến này và những hành động bị ràng buộc.

<!-- 
    With Data Binding, the root element is <layout>.  It contains 2 things.
    1. <data> - We define variables to which we wish to use in our binding expressions and 
                import any other classes we may need for reference, like android.view.View.
    2. <root layout> - This is the visual root layout of our view.  This is the root xml tag in the MVC and MVP view examples.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- We will reference the TicTacToeViewModel by the name viewModel as we have defined it here. -->
    <data>
        <import type="android.view.View" />
        <variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
    </data>
    <LinearLayout...>
        <GridLayout...>
            <!-- onClick of any cell in the board, the button clicked will invoke the onClickedCellAt method with its row,col -->
            <!-- The display value comes from the ObservableArrayMap defined in the ViewModel  -->
            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
                android:text='@{viewModel.cells["00"]}' />
            ...
            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> viewModel.onClickedCellAt(2,2)}"
                android:text='@{viewModel.cells["22"]}' />
        </GridLayout>

        <!-- The visibility of the winner view group is based on whether or not the winner value is null.
             Caution should be used not to add presentation logic into the view.  However, for this case
             it makes sense to just set visibility accordingly.  It would be odd for the view to render
             this section if the value for winner were empty.  -->
        <LinearLayout...
            android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
            tools:visibility="visible">

            <!-- The value of the winner label is bound to the viewModel.winner and reacts if that value changes -->
            <TextView
                ...
                android:text="@{viewModel.winner}"
                tools:text="X" />
            ...
        </LinearLayout>
    </LinearLayout>
</layout>

3.4 Evaluation

Unit test càng dễ dàng hơn, bởi vì không còn sự phụ thuộc vào view. Khi thử nghiệm, bạn chỉ cần xác minh rằng các biến observable được đặt phù hợp khi thay đổi model.

3.5 MVVM concern

  • Maintenance : Từ view có thể liên kết với cả hai biến và biểu thức, logic trình bày không liên quan có thể creep theo thời gian, thêm hiệu quả code XML của chúng ta. Để tránh điều này, luôn luôn có được giá trị trực tiếp từ các ViewModel hơn là cố gắng để tính toán hoặc lấy nó trong các view ràng buộc. Bằng cách này, việc tính toán có thể được unit test một cách thích hợp.

Nguồn: https://realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí