概述
(關於 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相關內容
在〈MVP Pattern in Android〉中有 1 則留言
[…] MVP Pattern in Android […]