MVP Pattern in Android

概述


(關於 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 完全分離不會有任何相依關係。

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

 

總結



  1. View 和 Model 不會有依賴

  2. View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法

  3. Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等

  4. View 會提供 Presenter 處理完邏輯後顯示內容的方法



Orignal From: MVP Pattern in Android

0 意見:

張貼留言

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Affiliate Network Reviews