1. 程式人生 > >Android 系統(77)---MVC,MVP,MVVM的區別

Android 系統(77)---MVC,MVP,MVVM的區別

MVC,MVP,MVVM的區別

一、MVC 軟體可以分為三部分

1.Model:模型層,負責處理資料的載入或者儲存 
2. View:檢視層,負責介面資料的展示,與使用者進行互動 
3.Controller:控制器層,負責邏輯業務的處理

各部分之間的通訊方式如下:

  1. View傳送指令到Controller
  2. Controller完成業務邏輯後,要求Model改變狀態
  3. Model將新的資料傳送到View,使用者得到反饋

Tips:所有的通訊都是單向的。

互動模式 接受使用者指令時,MVC可以分為兩種方式。一種是通過View接受指令,傳遞給Controller。

另一種是直接通過Controller接受指令

V層:應用層中處理資料顯示的部分,XML佈局可以視為V層,顯示Model層的資料結果。 

C層:在Android中,Activity處理使用者互動問題,因此可以認為Activity是控制器,Activity讀取V檢視層的資料(讀取當前EditText控制元件的資料),控制使用者輸入(EditText控制元件資料的輸入),並向Model傳送資料請求(發起網路請求等)。

M層:適合做一些業務邏輯處理,比如資料庫存取操作,網路操作,複雜的演算法,耗時的任務等都在model層處理。

二、MVP

1. Model: 資料層. 負責與網路層和資料庫層的邏輯互動.

2. View: UI層. 顯示資料, 並向Presenter報告使用者行為.

3. Presenter: 從Model拿資料, 應用到UI層, 管理UI的狀態, 決定要顯示什麼, 響應使用者的行為.

MVP模式將Controller改名為Presenter,同時改變了通訊方向。

1.各部分之間的通訊,都是雙向的

2.View和Model不發生聯絡,都通過Presenter傳遞

3.View非常薄,不部署任何業務邏輯,稱為"被動檢視"(Passive View),即沒有任何主動性,而Presenter非常厚,所有邏輯都部署在那裡

2.1 mvp 基本的Model-View-Presenter架構

app中有四個功能:

  • Tasks
  • TaskDetail
  • AddEditTask
  • Statistics

每個功能都有:

  • 一個定義View和Presenter介面的Contract介面;
  • 一個Activity用來管理fragment和presenter的建立;
  • 一個實現了View介面的Fragment;
  • 一個實現了Presenter介面的presenter.

基類

Presenter基類:

public interface BasePresenter {
    void start();
}

例子中這個start()方法都在Fragment的onResume()中呼叫.

View基類:

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

View實現

  • Fragment作為每一個View介面的實現, 主要負責資料顯示和在使用者互動時呼叫Presenter, 但是例子程式碼中也是有一些直接操作的部分, 比如點選開啟另一個Activity, 點選彈出選單(選單項的點選仍然是呼叫presenter的方法).
  • View介面中定義的方法多為showXXX()方法.

  • Fragment作為View實現, 介面中定義了方法:

    @Override
    public boolean isActive() {
      return isAdded();
    }

在Presenter中資料回撥的方法中, 先檢查View.isActive()是否為true, 來保證對Fragment的操作安全.

Presenter實現

  • Presenter的start()方法在onResume()的時候呼叫, 這時候取初始資料; 其他方法均對應於使用者在UI上的互動操作.
  • New Presenter的操作是在每一個Activity的onCreate()裡做的: 先添加了Fragment(View), 然後把它作為引數傳給了Presenter. 這裡並沒有存Presenter的引用.
  • Presenter的建構函式有兩個引數, 一個是Model(Model類一般叫XXXRepository), 一個是View. 構造中先用guava的checkNotNull()
    檢查兩個引數是否為null, 然後賦值到欄位; 之後再呼叫View的setPresenter()方法把Presenter傳回View中引用.

Model實現細節

  • Model只有一個類, 即TasksRepository. 它還是一個單例. 因為在這個應用的例子中, 我們操作的資料就這一份.

它由手動實現的注入類Injection類提供:

public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

構造如下:

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                        @NonNull TasksDataSource tasksLocalDataSource) {
    mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
    mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
}
  • 資料分為local和remote兩大部分. local部分負責資料庫的操作, remote部分負責網路. Model類中還有一個記憶體快取.
  • TasksDataSource是一個介面. 介面中定義了Presenter查詢資料的回撥介面, 還有一些增刪改查的方法.

單元測試

MVP模式的主要優勢就是便於為業務邏輯加上單元測試.
本例子中的單元測試是給TasksRepository和四個feature的Presenter加的.
Presenter的單元測試, Mock了View和Model, 測試呼叫邏輯, 如:

public class AddEditTaskPresenterTest {

    @Mock
    private TasksRepository mTasksRepository;
    @Mock
    private AddEditTaskContract.View mAddEditTaskView;
    private AddEditTaskPresenter mAddEditTaskPresenter;

    @Before
    public void setupMocksAndView() {
        MockitoAnnotations.initMocks(this);
        when(mAddEditTaskView.isActive()).thenReturn(true);
    }

    @Test
    public void saveNewTaskToRepository_showsSuccessMessageUi() {
        mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView);

        mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Description");

        verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model
        verify(mAddEditTaskView).showTasksList(); // shown in the UI
    }
    ...
}

2.2 mvp-loaders 用Loader取資料的MVP

基於上一個例子todo-mvp, 只不過這裡改為用Loader來從Repository得到資料.


todo-mvp-loaders

使用Loader的優勢:

  • 去掉了回撥, 自動實現資料的非同步載入;
  • 當內容改變時回調出新資料;
  • 當應用因為configuration變化而重建loader時, 自動重連到上一個loader.

Diff with todo-mvp

既然是基於todo-mvp, 那麼之前說過的那些就不再重複, 我們來看一下都有什麼改動:
git difftool -d todo-mvp

添加了兩個類:
TaskLoaderTasksLoader.

在Activity中new Loader類, 然後傳入Presenter的構造方法.

Contract中View介面刪掉了isActive()方法, Presenter刪掉了populateTask()方法.

資料獲取

新增的兩個新類是TaskLoaderTasksLoader, 都繼承於AsyncTaskLoader, 只不過資料的型別一個是單數, 一個是複數.

AsyncTaskLoader是基於ModernAsyncTask, 類似於AsyncTask,
把load資料的操作放在loadInBackground()裡即可, deliverResult()方法會將結果返回到主執行緒, 我們在listener的onLoadFinished()裡面就可以接到返回的資料了, (在這個例子中是幾個Presenter實現了這個介面).

TasksDataSource介面的這兩個方法:

List<Task> getTasks();
Task getTask(@NonNull String taskId);

都變成了同步方法, 因為它們是在loadInBackground()方法裡被呼叫.

Presenter中儲存了LoaderLoaderManager, 在start()方法裡initLoader, 然後onCreateLoader返回構造傳入的那個loader.
onLoadFinished()裡面呼叫View的方法. 此時Presenter實現LoaderManager.LoaderCallbacks.

資料改變監聽

TasksRepository類中定義了observer的介面, 儲存了一個listener的list:

private List<TasksRepositoryObserver> mObservers = new ArrayList<TasksRepositoryObserver>();

public interface TasksRepositoryObserver {
    void onTasksChanged();
}

每次有資料改動需要重新整理UI時就呼叫:

private void notifyContentObserver() {
    for (TasksRepositoryObserver observer : mObservers) {
        observer.onTasksChanged();
    }
}

在兩個Loader裡註冊和登出自己為TasksRepository的listener: 在onStartLoading()裡add, onReset()裡面remove方法.
這樣每次TasksRepository有資料變化, 作為listener的兩個Loader都會收到通知, 然後force load:

@Override
public void onTasksChanged() {
    if (isStarted()) {
        forceLoad();
    }
}

這樣onLoadFinished()方法就會被呼叫.

2.3 databinding

基於todo-mvp, 使用Data Binding library來顯示資料, 把UI和動作繫結起來.

說到ViewModel, 還有一種模式叫MVVM(Model-View-ViewModel)模式.
這個例子並沒有嚴格地遵循Model-View-ViewModel模式或者Model-View-Presenter模式, 因為它既用了ViewModel又用了Presenter.


mvp-databinding

Data Binding Library讓UI元素和資料模型繫結:

  • layout檔案用來繫結資料和UI元素;
  • 事件和action handler繫結;
  • 資料變為可觀察的, 需要的時候可以自動更新.

Diff with todo-mvp

添加了幾個類:

  • StatisticsViewModel;
  • SwipeRefreshLayoutDataBinding;
  • TasksItemActionHandler;
  • TasksViewModel;

從幾個View的介面可以看出方法數減少了, 原來需要多個showXXX()方法, 現在只需要一兩個方法就可以了.

資料繫結

TasksDetailFragment為例:
以前在todo-mvp裡需要這樣:

public void onCreateView(...) {
    ...
    mDetailDescription = (TextView)
root.findViewById(R.id.task_detail_description);
}

@Override
public void showDescription(String description) {
    mDetailDescription.setVisibility(View.VISIBLE);
    mDetailDescription.setText(description);
}

現在只需要這樣:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.taskdetail_frag, container, false);
    mViewDataBinding = TaskdetailFragBinding.bind(view);
    ...
}

@Override
public void showTask(Task task) {
    mViewDataBinding.setTask(task);
}

因為所有資料繫結的操作都寫在了xml裡:

<TextView
    android:id="@+id/task_detail_description"
    ...
    android:text="@{task.description}" />

事件繫結

資料繫結省去了findViewById()setText(), 事件繫結則是省去了setOnClickListener().

比如taskdetail_frag.xml中的

<CheckBox
    android:id="@+id/task_detail_complete"
    ...
    android:checked="@{task.completed}"
    android:onCheckedChanged="@{(cb, isChecked) ->
    presenter.completeChanged(task, isChecked)}" />

其中Presenter是這時候傳入的:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mViewDataBinding.setPresenter(mPresenter);
}

資料監聽

在顯示List資料的介面TasksFragment, 僅需要知道資料是否為空, 所以它使用了TasksViewModel來給layout提供資訊, 當尺寸設定的時候, 只有一些相關的屬性被通知, 和這些屬性繫結的UI元素被更新.

public void setTaskListSize(int taskListSize) {
    mTaskListSize = taskListSize;
    notifyPropertyChanged(BR.noTaskIconRes);
    notifyPropertyChanged(BR.noTasksLabel);
    notifyPropertyChanged(BR.currentFilteringLabel);
    notifyPropertyChanged(BR.notEmpty);
    notifyPropertyChanged(BR.tasksAddViewVisible);
}

其他實現細節

  • Adapter中的Data Binding, 見TasksFragment中的TasksAdapter.

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
      Task task = getItem(i);
      TaskItemBinding binding;
      if (view == null) {
          // Inflate
          LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
    
          // Create the binding
          binding = TaskItemBinding.inflate(inflater, viewGroup, false);
      } else {
          binding = DataBindingUtil.getBinding(view);
      }
    
      // We might be recycling the binding for another task, so update it.
      // Create the action handler for the view
      TasksItemActionHandler itemActionHandler =
              new TasksItemActionHandler(mUserActionsListener);
      binding.setActionHandler(itemActionHandler);
      binding.setTask(task);
      binding.executePendingBindings();
      return binding.getRoot();
    }
  • Presenter可能會被包在ActionHandler中, 比如TasksItemActionHandler.
  • ViewModel也可以作為View介面的實現, 比如StatisticsViewModel.
  • SwipeRefreshLayoutDataBinding類定義的onRefresh()動作繫結.

2.4 mvp-clean

這個例子在todo-mvp的基礎上, 加了一層domain層, 把應用分為了三層:


mvp-clean.png

Domain: 盛放了業務邏輯, domain層包含use cases或者interactors, 被應用的presenters使用. 這些use cases代表了所有從presentation層可能進行的行為.

關鍵概念
和基本的mvp sample最大的不同就是domain層和use cases. 從presenters中抽離出來的domain層有助於避免presenter中的程式碼重複.

Use cases定義了app需要的操作, 這樣增加了程式碼的可讀性, 因為類名反映了目的.

Use cases對於操作的複用來說也很好. 比如CompleteTask在兩個Presenter中都用到了.

Use cases的執行是在後臺執行緒, 使用command pattern. 這樣domain層對於Android SDK和其他第三方庫來說都是完全解耦的.

Diff with todo-mvp

每一個feature的包下都新增了domain層, 裡面包含了子目錄model和usecase等.

UseCase是一個抽象類, 定義了domain層的基礎介面點.
UseCaseHandler用於執行use cases, 是一個單例, 實現了command pattern.
UseCaseThreadPoolScheduler實現了UseCaseScheduler介面, 定義了use cases執行的執行緒池, 在後臺執行緒非同步執行, 最後把結果返回給主執行緒.
UseCaseScheduler通過構造傳給UseCaseHandler.
測試中用了UseCaseScheduler的另一個實現TestUseCaseScheduler, 所有的執行變為同步的.

Injection類中提供了多個Use cases的依賴注入, 還有UseCaseHandler用來執行use cases.

Presenter的實現中, 多個use cases和UsseCaseHandler都由構造傳入, 執行動作, 比如更新一個task:

private void updateTask(String title, String description) {
    if (mTaskId == null) {
        throw new RuntimeException("updateTask() was called but task is new.");
    }
    Task newTask = new Task(title, description, mTaskId);
    mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask),
            new UseCase.UseCaseCallback<SaveTask.ResponseValue>() {
                @Override
                public void onSuccess(SaveTask.ResponseValue response) {
                    // After an edit, go back to the list.
                    mAddTaskView.showTasksList();
                }

                @Override
                public void onError() {
                    showSaveError();
                }
            });
}

todo-mvp-dagger

關鍵概念:
dagger2 是一個靜態的編譯期依賴注入框架.
這個例子中改用dagger2實現依賴注入. 這樣做的主要好處就是在測試的時候我們可以用替代的modules. 這在編譯期間通過flavors就可以完成, 或者在執行期間使用一些除錯面板來設定.

Diff with todo-mvp

Injection類被刪除了.
添加了5個Component, 四個feature各有一個, 另外資料對應一個: TasksRepositoryComponent, 這個Component被儲存在Application裡.

資料的module: TasksRepositoryModulemockprod目錄下各有一個.

對於每一個feature的Presenter的注入是這樣實現的:
首先, 把Presenter的建構函式標記為@Inject, 然後在Activity中構造component並注入到欄位:

@Inject AddEditTaskPresenter mAddEditTasksPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.addtask_act);
    .....

    // Create the presenter
    DaggerAddEditTaskComponent.builder()
            .addEditTaskPresenterModule(
                    new AddEditTaskPresenterModule(addEditTaskFragment, taskId))
            .tasksRepositoryComponent(
                    ((ToDoApplication) getApplication()).getTasksRepositoryComponent()).build()
            .inject(this);
}

這個module裡provide了view和taskId:

@Module
public class AddEditTa