Chuyển đến nội dung chính

Design MVC, MVP và MVVM trong Android

The MVC, MVP, and MVVM Smackdown

Tic Tac Toe Game
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.






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.

Model

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

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

Controller

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:
Tic Tac Toe - MVC
Let’s examine the controller in more detail.
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("");
        }
    }
}

Evaluation

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 đề.

Controller Concerns

  • 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.
How can we address this? MVP to the rescue!

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.

Model

Tương tự MVC

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.

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.
Let’s again examine what that looks like in our app.
Tic Tac Toe - MVP
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.
public interface TicTacToeView {
    void showWinner(String winningPlayerDisplayLabel);
    void clearWinnerDisplay();
    void clearButtons();
    void setButtonText(int row, int col, String text);
}

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.

Presenter Concerns

  • 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.

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.
Let’s examine the parts of MVVM.

Model

Giống MVC

View

Giống MVC

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.
Tic Tac Toe - MVVM

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>
Nguồn: https://realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/

Nhận xét

Bài đăng phổ biến từ blog này

Jetpack Compose VS SwiftUI !VS Flutter

  Việc phát triển Android đã trở nên dễ dàng hơn khi các bản cập nhật liên tục đến. Sau bản cập nhật 2020.3.1, rất nhiều thứ đã thay đổi. Nhưng thay đổi chính mà tôi nghĩ hầu hết các nhà phát triển phải chờ đợi là Jetpack Compose cho ứng dụng sản xuất. Và Kotlin là lựa chọn duy nhất cho jetpack Compose, cũng là ngôn ngữ được ưu tiên. Để biết thêm chi tiết hoặc các thay đổi trên Jetpack Compose, bạn có thể truy cập vào https://developer.android.com/jetpack/compose Tương tự, IOS Development cũng cung cấp một tùy chọn để phát triển khai báo, SwiftUI. Trong IDE, không có thay đổi nào do điều này. Nhưng khái niệm gần giống với Jetpack Compose. Thay vì bảng phân cảnh, chúng tôi tạo giao diện người dùng bằng Swift. Để biết thêm chi tiết hoặc các thay đổi trên SwiftUI, hãy truy cập https://developer.apple.com/xcode/swiftui/ Hãy xem cách cả hai hoạt động bằng cách sử dụng một dự án demo. Tôi đã lấy một số ví dụ về số lần chạm tương tự của Flutter. 1. Android Jetpack Compose Chúng tôi có thể tạo

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolbars, slide panels, forms,

Một số bài tập Winform C#

Một số bài tập: 1. Mô phỏng game đoán số. Luật chơi:         o Đúng số và đúng vị trí   +         o Đúng số mà sai vị trí      ?         o Sai số và sai vị trí          -         . . .         - Kết quả được tạo ngẫu nhiên từ các số có 4 chữ số.         - Các chữ số có giá trị từ 0-6.         - Người chơi có 6 lần đoán. Chương trình tham khảo: 2. In số điện tử Yêu cầu: người dùng nhập vào 1 số ( hoặc 1 chuỗi số) yêu cầu in ra số đó dưới dạng số điện tử. Chương trình tham khảo: 3. Mô phỏng game CARO  (update) 4. Mô phỏng game DÒ MÌN (update)