概述
(關於 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 顯示內容。
- 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.  
      }  
    總結
- View 和 Model 不會有依賴
 - View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法
 - Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等
 - View 會提供 Presenter 處理完邏輯後顯示內容的方法
 
Orignal From: MVP Pattern in Android


0 意見:
張貼留言