概述
(關於 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 顯示內容。
- View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
- View和Presenter的互相關連是透過 setPresenter 方法,該方法會在 Presenter 的建構式呼叫。
- 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
- Presenter 其內部會持有 View 和 Model 的引用變數。
- View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
- Presenter的建構式會傳入 View 和 Model 並進行初始化。
- Presenter不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, TextView, Toast, Context 等等,只會處理業務邏輯,但可以依賴 RESULT_OK 之纇的整數常數。
- Presenter 會有一個 Model 的變數並從該變數來操作資料。
- 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的具體介紹參考這篇
總結
- View 和 Model 不會有依賴
- View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法
- Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等
- 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 的重點為
- 不可依賴 Model
- 和 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 的重點為
- 和 View 只透過介面方法交互,不可和實體方法交互
- 不可依賴 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
Article Comments