概述

(關於 MVC Pattern in Android 可以參考這篇,本篇是套用 MVP 於 Android 的描述和實作)
MVP 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Presenter(展示層)
View(視圖層): 負責與使用者互動並顯示來自展示層的資料。
Presenter(展示層): 負責處理視圖邏輯和互動邏輯。
Model(模型層): 負責管理業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念

當 Model 傳輸資料到 Presenter 時,Presenter 會封裝視圖邏輯後再把資料傳遞給 View。和 MVC 最大的不同為 MVP 將視圖邏輯從 Model 移動到 Presenter。而 Model 只保留業務邏輯,從而分離視圖邏輯和業務邏輯,且 View 和 Model 完全分離不會有任何相依關係。
View 和 Presenter 只透過介面的方法互相溝通,不依賴具體方法。
Presenter 不依賴於 Android 的 UI 元件,也就是在 Presenter 不會出現 Toast, Button, Context 等等,Presenter 處理完邏輯之後再呼叫 View 顯示內容。

Contract 介面

在 Google 所提供 MVP 範例中可以看到 View 和 Presenter 的介面互相對應,為了描述其相對應的關係,每對 View 和 Presenter 的介面都會放置於其 Contract 介面。e.g.,

public interface AddEditTaskContract {
    interface View extends BaseView<Presenter> {
        void showEmptyTaskError();
        void showTasksList();
        void setTitle(String title);
        void setDescription(String description);
        boolean isActive();
    }
    interface Presenter extends BasePresenter {
        void saveTask(String title, String description);
        void populateTask();
    }
}

BaseView 和 BasePresetner 介面定義了 View 層和 Presenter 層的公共接口,在 BaseView 會提供 setPresenter 方法以及泛型來讓 View 指定對應的 Presenter。e.g.,

public interface BaseView<T> {
    void setPresenter(T presenter);
}

BasePresenter 則是提供公共的啟動方法 start。e.g.,

public interface BasePresenter {
    void start();
}

View

在 MVP 架構中,View 和 Presenter 互相對應,View 用來顯示 Presenter 的資料。先將使用者輸入事件轉發給 Presenter,當 Presenter 處理完邏輯後,再呼叫 View 顯示內容。

  1. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  2. View和Presenter的互相關連是透過 setPresenter 方法,該方法會在 Presenter 的建構式呼叫。
  3. View 通常提供更新畫面的方法讓 Presenter 在執行完邏輯後呼叫。

在 Google 範例中 View 比較特別,它不是 Activity,而是一個自定義類別,該類別會使用自定義的 xml,xml 中會包含其它的 UI 元件。e.g.,
AddEditTaskView.java(View)

public class AddEditTaskView extends ScrollView implements AddEditTaskContract.View {
    private TextView mTitle;
    private TextView mDescription;
    private AddEditTaskContract.Presenter mPresenter;
    private boolean mActive;
    public AddEditTaskView(Context context) {
        super(context);
        init();
    }
    public AddEditTaskView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        inflate(getContext(), R.layout.addtask_view_content, this);
        mTitle = (TextView) findViewById(R.id.add_task_title);
        mDescription = (TextView) findViewById(R.id.add_task_description);
        mActive = true;
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mActive = true;
    }
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mActive = false;
    }
    @Override
    public void showEmptyTaskError() {
        Snackbar.make(mTitle,
                getResources().getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
    }
    @Override
    public void showTasksList() {
        Activity activity = getActivity(this);
        activity.setResult(Activity.RESULT_OK);
        activity.finish();
    }
    @Override
    public void setTitle(String title) {
        mTitle.setText(title);
    }
    @Override
    public void setDescription(String description) {
        mDescription.setText(description);
    }
    @Override
    public boolean isActive() {
        return mActive;
    }
    @Override
    public void setPresenter(AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }
    // TODO: This should be in the view contract
    public String getTitle() {
        return mTitle.getText().toString();
    }
    // TODO: This should be in the view contract
    public String getDescription() {
        return mDescription.getText().toString();
    }
}

在 init 方法中 R.layout.addtask_view_content = addtask_view_content.xml e.g.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">
        <EditText
            android:id="@+id/add_task_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/title_hint"
            android:singleLine="true"
            android:textAppearance="@style/TextAppearance.AppCompat.Title" />
        <EditText
            android:id="@+id/add_task_description"
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:gravity="top"
            android:hint="@string/description_hint" />
    </LinearLayout>
</merge>

簡單的說 View 是 Activity 內的一個 UI 元件,會以變數的方式存在於 Activity中。該變數的初始化也會使用 findViewById 方式初始化,初始化的位置在onCreate 中,而 View 和 Presenter 的相互關聯在 Presenter 的建構式。e.g.,
AddEditTaskActivity.java(這是 Activity 不是 View)

public class AddEditTaskActivity extends AppCompatActivity {
    public static final int REQUEST_ADD_TASK = 1;
    public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";
    private AddEditTaskPresenter mPresenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);
        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        checkNotNull(actionBar, "actionBar cannot be null");
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);
        final AddEditTaskView addEditTaskView =
                (AddEditTaskView) findViewById(R.id.add_edit_task_view);
        checkNotNull(addEditTaskView, "addEditTaskView not found in layout");
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_edit_task_done);
        checkNotNull(fab, "fab not found in layout");
        fab.setImageResource(R.drawable.ic_done);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /*
                 * TODO:
                 * View listeners should simply report the event to the presenter.
                 * In this case: mPresenter.onSavePressed()
                 */
                mPresenter.saveTask(addEditTaskView.getTitle(), addEditTaskView.getDescription());
            }
        });
        String taskId = null;
        if (getIntent().hasExtra(ARGUMENT_EDIT_TASK_ID)) {
            taskId = getIntent().getStringExtra(ARGUMENT_EDIT_TASK_ID);
            actionBar.setTitle(R.string.edit_task);
        } else {
            actionBar.setTitle(R.string.add_task);
        }
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
    }
    @Override
    protected void onResume() {
        super.onResume();
        mPresenter.start();
    }
    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }
    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}

Presenter

  1. Presenter 其內部會持有 View 和 Model 的引用變數。
  2. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  3. Presenter的建構式會傳入 View 和 Model 並進行初始化。
  4. Presenter不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, TextView, Toast, Context 等等,只會處理業務邏輯,但可以依賴 RESULT_OK 之纇的整數常數。
  5. Presenter 會有一個 Model 的變數並從該變數來操作資料。
  6. Presenter 通常是處理完邏輯之後,再呼叫 View 的介面更新 UI。

e.g., AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask()) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

TasksDataSource mTasksRepository 就是 Model。在建構式中傳入 View 和 Model,並呼叫 View 的 setPresenter 綁定 Presenter 自己。

從 updateTask 方法可以看到 先呼叫 Model 改變資料(mTasksRepository.saveTask)
接著再呼叫 View 顯示內容(mAddTaskView.showTasksList)

e.g.,

    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }

Model

Model 為 TaskRepository,TaskRepository 繼承 TasksDataSource,而 TaskDataSource 實際上是一個 interface,其中 2 個內部介面 LoadTasksCallback 和 GetTasksCallback 用來作 callback 使用,內部介面的 onTasksLoaded 方法用來當取得 task 成功之後把 task 傳回呼叫點的用途,而 onDataNotAvailable 方法用來當取得 task 失敗後的後續處理。
其餘在 TasksDataSource 介面的方法都是改變資料的共用方法,只要是 Model 都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

關於Model的具體介紹參考這篇

總結

  1. View 和 Model 不會有依賴
  2. View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法
  3. Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等
  4. View 會提供 Presenter 處理完邏輯後顯示內容的方法

接下來使用 MVP 改寫 MVC Pattern in Android 的亂數產生器

Contract

首先建立 MVPContract 介面,該介面中一樣包含 View 和 Presenter 兩個互相對應的內部介面,View 介面一開始可能只有 setPresenter 方法隨後跟著具體子類別的完成再逐漸提取方法到介面內。e.g.,

public interface MVPContract {
  interface View {
    void setPresenter(Presenter presenter);
    void setRollNumber(int randomNumber);
  }
  interface Presenter {
    void rollIt();
  }
}

View

View 的部分直接讓 Activity 去繼承 MVPContract.View,並在內持有 MVPContract.Presenter 的變數並複寫 setPresenter 方法。
關於 MVP View 的重點為

  1. 不可依賴 Model
  2. 和 Presenter 只透過介面的方法交互,不可和實體方法交互

e.g.,

public class MVPActivity extends AppCompatActivity implements OnClickListener, MVPContract.View {
  //Main UI
  private Button mRollIt;
  private TextView mRollNumber;
  private MVPContract.Presenter mPresenter;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mvp_activity);
    initData();
    initUI();
  }
  private void initData() {
    mPresenter = new MVPPresenter(this, new MVPModel());
  }
  private void initUI() {
    mRollIt = findViewById(R.id.mvp_roll_it);
    mRollIt.setOnClickListener(this);
    mRollNumber = findViewById(R.id.mvp_show_roll_number);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.mvp_roll_it:
        mPresenter.rollIt();
        break;
    }
  }
  @Override
  public void setPresenter(MVPContract.Presenter presenter) {
    mPresenter = presenter;
  }
  @Override
  public void setRollNumber(int randomNumber) {
    mRollNumber.setText(""+randomNumber);
  }
}

Presenter

建立 MVPPresenter 繼承 MVPContract.Presenter,在建構式內關連 View 和 Model。
關於 MVP Presenter 的重點為

  1. 和 View 只透過介面方法交互,不可和實體方法交互
  2. 不可依賴 Android 的 UI 元件,如 Button,EditText,Context 等等
public class MVPPresenter implements MVPContract.Presenter {
  private MVPContract.View mView;
  private MVPModel mModel;
  public MVPPresenter(MVPContract.View view, MVPModel model) {
    mView = view;
    mModel = model;
    mView.setPresenter(this);
  }
  @Override
  public void rollIt() {
    mModel.rollOnce();
    mView.setRollNumber(mModel.getRandomNumber());
  }
}

Model

最後是 Model,Model 並沒有變化。

public class MVPModel {
  private int mRandomNumber;
  public void rollOnce() {
    mRandomNumber = new Random().nextInt(100);
  }
  public int getRandomNumber() {
    return mRandomNumber;
  }
}

以上為MVP Pattern in Android的範例。


以下為MVP相關內容
http://34.80.81.192/?p=6021