從Google的todo-mvp源碼中學習MVP模式

從Google的todo-mvp源碼中學習MVP模式

MVP : Model-View-Presenter

MVP模式可以說是現在開發模式中的mvp了。MVP模式也是mvvm模式的一個基礎。
Google在Github上面公佈了一組官方的mvp samples:Google todo-mvp

通過對以下幾個例子的源碼分析,來深入學習MVP模式。

Sample Description
todo‑mvp 最基礎的MVP模式用例
todo‑mvp‑clean 使用了 Clean Architecture 的概念
todo‑mvp‑dagger 使用Dagger 2進行依賴注入
todo‑mvp‑rxjava 使用Rxjava 2進行併發操作
todo‑mvvm‑databinding 通過databinding來轉變成mvvm框架

上述的幾個例子都是基於第一個todo-mvp這個例子的基礎改變而成的。

首先來對比一下MVP模式和MVC模式。

借用

MVC模式:在安卓中,Activity即作爲Controller又作爲View的一部分,這就導致了代碼耦合。
MVP模式:就是解決上述的問題,把Activity和XML頁面都僅作爲View,Controller的功能以及操作Model都交給了Presenter。通過Presenter把View和Model徹底分開。


todo‑mvp

Github地址

先看看官方項目介紹中的結構圖:
這裏寫圖片描述
其中Fragment作爲View的部分,Presenter是一個自定義的類
而左邊的一塊,Repository就是Model部分了。

我們的分析先從整體結構入手,然後局部源碼分析:

首先看一下項目結構:這裏寫圖片描述

這裏主要看main目錄下的文件結構:
其中tasks,addedittask,statistics,taskdetail這4個目錄都是獨立且相似的。
因此我們就分析tasks目錄下的4個主要文件(這4個文件組成了MVP模式中一個頁面實現的標準結構):
TasksActivity: 初始化View(TasksFragment)和Presenter(TasksPresenter)
TasksContract: 接口,裏面有兩個內部接口View接口和Presenter接口。
TaskFragment: MVP模式,View的部分(implements TasksContract.View)
TaskPresenter: MVP模式,Presenter的部分(implements TasksContract.Presenter)

我們在看看data目錄下的文件:
TasksDataSource:Model操作的接口,定義了Model的相關操作
TasksRepository:操作Model的一個實例,實現了TasksDataSource接口,裏面還包含了兩個成員:mTasksRemoteDataSource,mTasksLocalDataSource,分別代表本地Model操作和遠程Model操作(從網絡獲取數據)
Task:一個具體的Model

我們再從一個簡單的UML圖中看看結構:
這裏寫圖片描述
我們可以清晰的看到View和Presenter分別持有對方的接口對象,這樣子就能互相調用。但是View對於Model層是完全被隔斷的。
Presenter同時持有Model和View的對象。
而Model對Presenter和View都是沒有感知的。

看完了宏觀的結構,我們從代碼層次微觀的更細緻的分析一下:

1,首先從TasksActivity的onCreate()方法開始:

//TasksActivity.java

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tasks_act);

        // Set up the toolbar.
        ……………………………………………………

        // Set up the navigation drawer.
       ……………………………………………………

        TasksFragment tasksFragment =
                (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (tasksFragment == null) {
            // Create the fragment
            tasksFragment = TasksFragment.newInstance();
            ActivityUtils.addFragmentToActivity(
                    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
        }

        // Create the presenter
        mTasksPresenter = new TasksPresenter(
                Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

        // Load previously saved state, if available.
        if (savedInstanceState != null) {
            TasksFilterType currentFiltering =
                    (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
            mTasksPresenter.setFiltering(currentFiltering);
        }
    }

在onCreate()方法中,主要創建了TasksFragment和TasksPresenter,並對保存在savedInstanceState的數據進行處理。其中以下這行代碼是最重要的:

        mTasksPresenter = new TasksPresenter(
             Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

通過TasksPresenter的構造方法,把Model View Presenter聯繫在了一起。
其中Injection是通過依賴注入的方式構造了一個TasksRepository對象,具體如何構造先不管他。只要知道TasksPresenter通過這個TasksRepository對象來操作Model的。

//TasksPresenter.java
public class TasksPresenter implements TasksContract.Presenter {

    private final TasksRepository mTasksRepository;

    private final TasksContract.View mTasksView;
    …………………………
    public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
        mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
        mTasksView.setPresenter(this);
    }
   …………………………
}

在TasksPresenter的構造函數中,把TasksRepository和TasksFragment賦值給自己的成員變量。並調用TasksFragment的setPresenter方法,把自己的引用交給他。這樣子,MVP三者就互相關聯了。

然後就開始正式的運作了:
2,在剛纔的onCreate方法中提交了Fragment自然會運行到TasksFragment的生命週期onCreateView方法:

//TasksFragment.java

public class TasksFragment extends Fragment implements TasksContract.View {

    private TasksContract.Presenter mPresenter;
    private TasksAdapter mListAdapter;
    private View mNoTasksView;
    private ImageView mNoTaskIcon;

……………………
    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

    …………………………
}

這裏就撿去了一個關鍵代碼onResume()方法,在該生命週期中,調用了mPresenter的start()方法,該方法時實現的BasePresenter接口的一個方法。
在TasksFragment.java的代碼中,我們可以發現所有和數據相關的操作都是交給了mPresenter成員變量去實現。在TasksFragment.java中只剩下了獲取view對象,view對象的一些基本設置。

3,我們知道這個mPresenter對象的實例是TasksPresenter對象,因此,我們查看TasksPresenter中的start()方法:
最終調用了loadTasks方法,
在loadTasks方法中,TasksPresenter 通過mTasksRepository成員變量,調用mTasksRepository.getTasks()方法去獲取Model中的數據。其中的參數是一個callback,就是告訴Model倉庫,獲取到了Model以後,需要使用TasksPresenter的processTasks方法去處理獲取到的數據。

ublic class TasksPresenter implements TasksContract.Presenter {

    private final TasksRepository mTasksRepository;

    private final TasksContract.View mTasksView;

    ……………………
    @Override
    public void start() {
        loadTasks(false);
    }


    @Override
    public void loadTasks(boolean forceUpdate) {
        // Simplification for sample: a network reload will be forced on first load.
        loadTasks(forceUpdate || mFirstLoad, true);
        mFirstLoad = false;
    }

    /**
     * @param forceUpdate   Pass in true to refresh the data in the {@link TasksDataSource}
     * @param showLoadingUI Pass in true to display a loading icon in the UI
     */
    private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
        if (showLoadingUI) {
            mTasksView.setLoadingIndicator(true);
        }
        if (forceUpdate) {
            mTasksRepository.refreshTasks();
        }

        // The network request might be handled in a different thread so make sure Espresso knows
        // that the app is busy until the response is handled.
        EspressoIdlingResource.increment(); // App is busy until further notice

        mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                List<Task> tasksToShow = new ArrayList<Task>();

                // This callback may be called twice, once for the cache and once for loading
                // the data from the server API, so we check before decrementing, otherwise
                // it throws "Counter has been corrupted!" exception.
                if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
                    EspressoIdlingResource.decrement(); // Set app as idle.
                }

                // We filter the tasks based on the requestType
                for (Task task : tasks) {
                    switch (mCurrentFiltering) {
                        case ALL_TASKS:
                            tasksToShow.add(task);
                            break;
                        case ACTIVE_TASKS:
                            if (task.isActive()) {
                                tasksToShow.add(task);
                            }
                            break;
                        case COMPLETED_TASKS:
                            if (task.isCompleted()) {
                                tasksToShow.add(task);
                            }
                            break;
                        default:
                            tasksToShow.add(task);
                            break;
                    }
                }
                // The view may not be able to handle UI updates anymore
                if (!mTasksView.isActive()) {
                    return;
                }
                if (showLoadingUI) {
                    mTasksView.setLoadingIndicator(false);
                }

                processTasks(tasksToShow);
            }

      ……………………

}

4,首先判斷緩存中是否有數據且緩存數據不髒(沒被修改過),如果有數據則直接通過callback提交回Presenter中,
如果數據已經不是最新數據了(被修改過了),通過從遠程獲取最新的數據,如果緩存中沒數據,但數據是最新的,那麼直接從本地讀取數據

//TasksRepository.java
public class TasksRepository implements TasksDataSource {

    private static TasksRepository INSTANCE = null;

    private final TasksDataSource mTasksRemoteDataSource;

    private final TasksDataSource mTasksLocalDataSource;
    …………………………………………………………
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);

        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }

        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }

                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }
    ………………………………………………………………
}    

總結:
1,創建presenter和view的接口
2,在activity中創建fragment作爲view
3,在activity中創建presenter,並與之前的view互相綁定
4,在fragment的onresume生命週期中通過presenter初始化數據。即從model中獲取數據。


todo‑mvp-clean

Github地址

同樣先看看官方的配圖:
這裏寫圖片描述

我們發現就在Presenter和Model的中間,多了一塊Domain。

該項目是根據Clean Architecture的原則來實現的一個MVP模式的項目。
Clean Architecture簡單的說就是一個分層模型,外層擁有內層對象的引用,因此外層可以通過內層暴露的方法進行需求的操作。而內層對外層是一無所知的。
這篇文章http://www.jianshu.com/p/cba6663435c7有個不錯的解說。

我們主要來看看 這個多出來的Domain到底是什麼。
Domian其實就是所有數據操作的集合。簡單的說,就是增刪改查這些操作。
每一個Use case就是一種操作。

來看一下項目結構目錄:
這裏寫圖片描述
對比例一的項目結構,我們可以發現 多出了一個domain文件目錄,多出了UseCase抽象類,UseCaseHandler類,UseCaseScheduler接口和UseCaseThreadPoolScheduler類。

再來對比一下一個簡單的UML圖:
這裏寫圖片描述
我們可以發現Domain部分,就是由UseCase(具體任務),UseCaseHandler(任務分發處理器),UseCaseThreadPoolScheduler(任務調度執行器)三個部分構成的。

UseCase抽象類:每一個具體任務(操作)都繼承自這個抽象類,要實現executeUseCase抽象方法,通過泛型約定了這個具體任務的請求參數類型(request)和響應數據類型(response)。

UseCaseHandler類:Usecase任務的處理器,分發和處理每一個不同的UseCase,把UseCase交給調度執行器(UseCaseScheduler)去執行。

UseCaseThreadPoolScheduler類:調度執行器,實現了UseCaseScheduler接口,裏面主要包含一個線程池,通過線程池去執行UseCase任務。

domain文件目錄:裏面主要有一個useCase文件夾。裏面每一個XxxTask都是繼承自UseCase抽象類。

接着看一下源代碼是怎麼執行的:
1,同樣從TasksActivity的onCreate方法開始,其中不同的就是創建TasksPresenter的時候,參數變多了,把所有UseCase和UseCaseHandler都通過參數的方式,傳遞給TaskPresenter,這樣TaskPresenter就可以用UseCaseHandler來處理UseCase任務。

//TaskActivity.java
        // Create the presenter
        mTasksPresenter = new TasksPresenter(
                Injection.provideUseCaseHandler(),
                tasksFragment,
                Injection.provideGetTasks(getApplicationContext()),
                Injection.provideCompleteTasks(getApplicationContext()),
                Injection.provideActivateTask(getApplicationContext()),
                Injection.provideClearCompleteTasks(getApplicationContext())
                );

2,同樣在TasksFragment的onResume生命週期中調用剛纔的TasksPresenter的start()接口方法,最終將調用TasksFragment的loadTasks方法。

//TasksPresenter.java

   /**
     * @param forceUpdate   Pass in true to refresh the data in the {@link TasksDataSource}
     * @param showLoadingUI Pass in true to display a loading icon in the UI
     */
    private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
        if (showLoadingUI) {
            mTasksView.setLoadingIndicator(true);
        }

        GetTasks.RequestValues requestValue 
        = new GetTasks.RequestValues(forceUpdate,
                mCurrentFiltering);

        mUseCaseHandler.execute(mGetTasks, requestValue,
                new UseCase.UseCaseCallback<GetTasks.ResponseValue>() {
                    @Override
                    public void onSuccess(GetTasks.ResponseValue response) {
                        List<Task> tasks = response.getTasks();
                        // The view may not be able to handle UI updates anymore
                        if (!mTasksView.isActive()) {
                            return;
                        }
                        if (showLoadingUI) {
                            mTasksView.setLoadingIndicator(false);
                        }

                        processTasks(tasks);
                    }

                    @Override
                    public void onError() {
                        // The view may not be able to handle UI updates anymore
                        if (!mTasksView.isActive()) {
                            return;
                        }
                        mTasksView.showLoadingTasksError();
                    }
                });
    }

該方法中主要是通過UseCaseHandler對象來處理UseCase任務。mUseCaseHandler的execute方法的參數分別是指定的任務GetTasks(UseCase),該任務涉及的請求參數,及其執行完任務以後的回調函數。

3,mUseCaseHandler的execute方法會調用mUseCaseScheduler(UseCaseScheduler)的execute接口方法。execute方法的參數是一個Runable對象,是爲了能在接下來的線程中進行調用。這個Runable裏面主要就是調用了UseCase的run方法。然後,mUseCaseScheduler的具體實例是UseCaseThreadPoolScheduler對象。在UseCaseThreadPoolScheduler中會創建一個ThreadPoolExecutor,最後的執行就是交給這個ThreadPoolExecutor線程池對象來執行的。


//UseCaseHandler.java

public class UseCaseHandler {

    private static UseCaseHandler INSTANCE;

    private final UseCaseScheduler mUseCaseScheduler;

    public UseCaseHandler(UseCaseScheduler useCaseScheduler) {
        mUseCaseScheduler = useCaseScheduler;
    }

    public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute(
            final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) {
        useCase.setRequestValues(values);
        useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this));

        // The network request might be handled in a different thread so make sure
        // Espresso knows
        // that the app is busy until the response is handled.
        EspressoIdlingResource.increment(); // App is busy until further notice

        mUseCaseScheduler.execute(new Runnable() {
            @Override
            public void run() {

                useCase.run();
                // This callback may be called twice, once for the cache and once for loading
                // the data from the server API, so we check before decrementing, otherwise
                // it throws "Counter has been corrupted!" exception.
                if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
                    EspressoIdlingResource.decrement(); // Set app as idle.
                }
            }
        });
    }
………………
}

//UseCaseThreadPoolScheduler.java
public class UseCaseThreadPoolScheduler implements UseCaseScheduler {
    ThreadPoolExecutor mThreadPoolExecutor;
    @Override
    public void execute(Runnable runnable) {
        mThreadPoolExecutor.execute(runnable);
    }
}

4,因此,最終會在一個新線程中調用UseCase的run方法。run方法中會執行真正實現類的executeUseCase方法,在executeUseCase方法中我們可以看到與例一中相似的代碼,去請求Model中的數據。

//UseCase.java
public abstract class UseCase<………………> {
   void run() {
       executeUseCase(mRequestValues);
    }
    protected abstract void executeUseCase(Q requestValues);
}

//GetTasks.java
public class GetTasks extends UseCase<GetTasks.RequestValues, GetTasks.ResponseValue> {

    private final TasksRepository mTasksRepository;
    ……………………
    @Override
    protected void executeUseCase(final RequestValues values) {
        if (values.isForceUpdate()) {
            mTasksRepository.refreshTasks();
        }

        mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                TasksFilterType currentFiltering = values.getCurrentFiltering();
                TaskFilter taskFilter = mFilterFactory.create(currentFiltering);

                List<Task> tasksFiltered = taskFilter.filter(tasks);
                ResponseValue responseValue = new ResponseValue(tasksFiltered);
                getUseCaseCallback().onSuccess(responseValue);
            }

            @Override
            public void onDataNotAvailable() {
                getUseCaseCallback().onError();
            }
        });

    }
}

因此,實際上,就多封裝了一層Domain層,Domain層進行多種任務的調度和管理。


todo‑mvp‑dagger

Github地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章