Google官方MVP示例代碼閱讀筆記
剝絲抽繭,理清項(xiàng)目結(jié)構(gòu)
國際慣例,上項(xiàng)目結(jié)構(gòu)圖:
從包名上很容易分辨出功能:addedittask是添加任務(wù),data是數(shù)據(jù)管理,statistics是統(tǒng)計(jì),taskdetail是任務(wù)詳情,tasks是任務(wù)瀏覽之類的。事實(shí)上這個項(xiàng)目的關(guān)鍵也就是: Tasks 、 TaskDetail 、 AddEditTask 、 Statistics 。
這四個關(guān)鍵的地方都有相同之處:
- 定義了view和presenter的契約
- Activity負(fù)責(zé)fragment和presenter的創(chuàng)建
- Fragment實(shí)現(xiàn)了view接口
- presenter實(shí)現(xiàn)了presenter接口
也就是說,幾個功能每一個都是MVP的模式,只不過Model層是公用的。而且這個項(xiàng)目里View層都是Fragment,果然google推薦用Fragment自己的項(xiàng)目里也給我們做個示范……其實(shí)關(guān)于到底是不是要用Fragment,還是有些爭議的,那么到底要不要用呢?我覺得對于個體而言,不管你喜不喜歡,都要用一用,試一試,因?yàn)槿艘砷L,必須踩坑。對于正式項(xiàng)目而言,則需要綜合考量,使用Fragment的利是否大于弊。
扯遠(yuǎn)了,接下來看一下他代碼倉庫給的一張結(jié)構(gòu)圖:
可以看出來左邊是數(shù)據(jù)管理,典型的Model層。而右邊呢,你可能認(rèn)為Activity是Presenter,事實(shí)上并不是,Presenter在Activity內(nèi),F(xiàn)ragment是View無疑。到這,我覺得關(guān)于這個項(xiàng)目結(jié)構(gòu)的簡介已經(jīng)足夠了,接下來看代碼。
我覺得看一個Android項(xiàng)目的正確姿勢應(yīng)該是先把玩一下app,看一下功能。貼幾張app的圖:
接著就該上入口的Activity看一下了,這個項(xiàng)目的入口Activity是TasksActivity,所在的包是tasks,看一下有哪些東西:
***個是自定義View,第二個就是入口Activity了,第三個即上面所說的“契約”,里面包含了View接口和Presenter接口。TasksFilterType則是一個枚舉,里面有三個過濾類型:所有,進(jìn)行中的,完成的。TasksFragment就是MVP中的View了,TasksPresenter則是MVP中的Presenter了。看一下TasksActivity中的初始化代碼:
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.tasks_act);
- Log.e(getClass().getSimpleName(),"onCreate");
- // Set up the toolbar.
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- ActionBar ab = getSupportActionBar();
- ab.setHomeAsUpIndicator(R.drawable.ic_menu);
- ab.setDisplayHomeAsUpEnabled(true);
- /**
- * 以下的DrawerLayout暫時不看了
- */
- // Set up the navigation drawer.
- mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
- mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);
- NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
- if (navigationView != null) {
- setupDrawerContent(navigationView);
- }
- // 獲取fragment并將之添加到視圖上
- // 懸浮按鈕在這個taksFragment里設(shè)置的點(diǎn)擊事件
- TasksFragment tasksFragment =
- (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
- // getSupportFragmentManager().findFragmentById()
- if (tasksFragment == null) {
- // Create the fragment
- tasksFragment = TasksFragment.newInstance();
- // 提供方法幫助activity加載ui
- // 這個方法其實(shí)就是拿到一個事務(wù),然后把這個fragment add到對應(yīng)的id上了
- 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);
- }
- }
首先是初始化toolbar和側(cè)滑,這里不必深入細(xì)節(jié),可以跳過這倆。之后初始化fragment和presenter,初始化Fragment先是嘗試通過id尋找可能已經(jīng)存在的Fragment對象,如果沒有,則重新創(chuàng)建一個Fragment對象。下一步則是創(chuàng)建一個presenter,***則是讓應(yīng)用在橫豎屏狀態(tài)切換的情況下恢復(fù)數(shù)據(jù)。
接下來看一下View和Presenter的“契約”:
- public interface TasksContract {
- interface View extends BaseView<Presenter> {
- void setLoadingIndicator(boolean active);
- void showTasks(List<Task> tasks);
- void showAddTask();
- void showTaskDetailsUi(String taskId);
- void showTaskMarkedComplete();
- void showTaskMarkedActive();
- void showCompletedTasksCleared();
- void showLoadingTasksError();
- void showNoTasks();
- void showActiveFilterLabel();
- void showCompletedFilterLabel();
- void showAllFilterLabel();
- void showNoActiveTasks();
- void showNoCompletedTasks();
- void showSuccessfullySavedMessage();
- boolean isActive();
- void showFilteringPopUpMenu();
- }
- interface Presenter extends BasePresenter {
- void result(int requestCode, int resultCode);
- void loadTasks(boolean forceUpdate);
- void addNewTask();
- void openTaskDetails(@NonNull Task requestedTask);
- void completeTask(@NonNull Task completedTask);
- void activateTask(@NonNull Task activeTask);
- void clearCompletedTasks();
- void setFiltering(TasksFilterType requestType);
- TasksFilterType getFiltering();
- }
- }
這個接口里包含了View和Presenter,可以看到View和Presenter里的方法比較多,事實(shí)上這是應(yīng)該的。因?yàn)樵贛VP架構(gòu)里,View只負(fù)責(zé)根據(jù)Presenter的指示繪制UI,View將所有的用戶交互交給Presenter處理。所以Presenter的很多方法可能就是對用戶的輸入的處理,而有輸入必然有輸出,View接口定義的各個方法便是給Presenter回調(diào)的。Presenter通過回調(diào)函數(shù)將對用戶的輸入的處理結(jié)果推到View中,View再根據(jù)這個結(jié)果對UI進(jìn)行相應(yīng)的更新。而在此項(xiàng)目中,F(xiàn)ragment就是View,在Fragment的各個點(diǎn)擊事件中都調(diào)用了Presenter的對應(yīng)方法,將業(yè)務(wù)邏輯交給Presenter處理。這看起來比傳統(tǒng)的MVC強(qiáng)上很多,因?yàn)閭鹘y(tǒng)MVC中Activity既可以認(rèn)為是Controller亦可以認(rèn)為是View,職責(zé)難以分離,寫到后面可能一個Activity就有上千行的代碼,這會為后續(xù)的維護(hù)帶來不少麻煩。而MVP則將業(yè)務(wù)邏輯抽取到了Presenter中,作為View的Fragment或者Activity職責(zé)更加單一,無疑為后續(xù)的開發(fā)維護(hù)帶來了便利。
接下來詳細(xì)的看Presenter的初始化,Presenter的創(chuàng)建是在TasksActivity中完成的,查看其構(gòu)造函數(shù):
- 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);
- }
前兩個檢查傳入的參數(shù)是否為空,接著將其賦值給TasksPresenter內(nèi)的引用,調(diào)用view的setPresenter方法,將自身傳入,這樣view中就可以使用presenter對象了,比直接從activity中拿看起來要優(yōu)雅了不少。Presenter具體的邏輯就不看了,都是一些比較簡單的代碼,回顧一下打開這個app所發(fā)生的事件的流程:創(chuàng)建TasksActivity -> 初始化Toolbar -> 初始化側(cè)滑 -> 創(chuàng)建TasksFragment對象 -> 創(chuàng)建TaskPresenter對象 -> 給Fragment設(shè)置Presenter對象 -> 初始化Fragment布局,這樣一套流程下來,整個流程就理清了,接下來只是等待用戶的輸入了。
接下來要看的是從本文開始到現(xiàn)在都一直忽略了的Model:TasksRepository。不過在分析TasksRepository之前,安利一下這個項(xiàng)目里的實(shí)體類,寫的比較優(yōu)雅,我們平時寫實(shí)體類時***也能按照他的套路來寫。我為什么說他寫的比較優(yōu)雅呢?因?yàn)楦鱾€屬性或者是帶返回值的方法都打上了@Nullable或者@NoNull注解來說明是否可以為空,事實(shí)上空指針這個錯可以算是平時經(jīng)常遇到的錯了……不過如果你有良好的設(shè)計(jì)和編碼習(xí)慣,是可以避免的,帶上這兩個注解可以在編譯期給你相關(guān)的提示。不僅如此,這個實(shí)體類還復(fù)寫了equals()、hashCode()和toString()方法,而且實(shí)現(xiàn)的方式也符合規(guī)范,關(guān)于如何復(fù)寫這三個方法,在《effective java》上有很好的總結(jié),各位可以去讀一下。
- /*
- * Copyright 2016, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.example.android.architecture.blueprints.todoapp.data;
- import android.support.annotation.NonNull;
- import android.support.annotation.Nullable;
- import com.google.common.base.Objects;
- import com.google.common.base.Strings;
- import java.util.UUID;
- /**
- * Immutable model class for a Task.
- */
- public final class Task {
- @NonNull
- private final String mId;
- @Nullable
- private final String mTitle;
- @Nullable
- private final String mDescription;
- private final boolean mCompleted;
- /**
- * Use this constructor to create a new active Task.
- *
- * @param title title of the task
- * @param description description of the task
- */
- public Task(@Nullable String title, @Nullable String description) {
- this(title, description, UUID.randomUUID().toString(), false);
- }
- /**
- * Use this constructor to create an active Task if the Task already has an id (copy of another
- * Task).
- *
- * @param title title of the task
- * @param description description of the task
- * @param id id of the task
- */
- public Task(@Nullable String title, @Nullable String description, @NonNull String id) {
- this(title, description, id, false);
- }
- /**
- * Use this constructor to create a new completed Task.
- *
- * @param title title of the task
- * @param description description of the task
- * @param completed true if the task is completed, false if it's active
- */
- public Task(@Nullable String title, @Nullable String description, boolean completed) {
- this(title, description, UUID.randomUUID().toString(), completed);
- }
- /**
- * Use this constructor to specify a completed Task if the Task already has an id (copy of
- * another Task).
- *
- * @param title title of the task
- * @param description description of the task
- * @param id id of the task
- * @param completed true if the task is completed, false if it's active
- */
- public Task(@Nullable String title, @Nullable String description,
- @NonNull String id, boolean completed) {
- mId = id;
- mTitle = title;
- mDescription = description;
- mCompleted = completed;
- }
- @NonNull
- public String getId() {
- return mId;
- }
- @Nullable
- public String getTitle() {
- return mTitle;
- }
- @Nullable
- public String getTitleForList() {
- if (!Strings.isNullOrEmpty(mTitle)) {
- return mTitle;
- } else {
- return mDescription;
- }
- }
- @Nullable
- public String getDescription() {
- return mDescription;
- }
- public boolean isCompleted() {
- return mCompleted;
- }
- public boolean isActive() {
- return !mCompleted;
- }
- public boolean isEmpty() {
- return Strings.isNullOrEmpty(mTitle) &&
- Strings.isNullOrEmpty(mDescription);
- }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Task task = (Task) o;
- return Objects.equal(mId, task.mId) &&
- Objects.equal(mTitle, task.mTitle) &&
- Objects.equal(mDescription, task.mDescription);
- }
- @Override
- public int hashCode() {
- return Objects.hashCode(mId, mTitle, mDescription);
- }
- @Override
- public String toString() {
- return "Task with title " + mTitle;
- }
- }
先看一下TasksRepository所在的包的結(jié)構(gòu):
可以從包名上看出local是從本地讀取數(shù)據(jù),remote是遠(yuǎn)程讀取,當(dāng)然了,這里只是模擬遠(yuǎn)程讀取。本地采用了數(shù)據(jù)庫存取的方式。在TasksRepository(下文簡稱TR)內(nèi)部有兩個TasksDataSource的引用:
- private final TasksDataSource mTasksRemoteDataSource;
- private final TasksDataSource mTasksLocalDataSource;
TasksDataSource是data包內(nèi)的一個接口,使用接口引用,無非是想解耦,就算以后需求變更,不想采用數(shù)據(jù)庫的方式存儲數(shù)據(jù),只要實(shí)現(xiàn)了這個接口,TR內(nèi)部的代碼也無需變更。TR用了單例,實(shí)現(xiàn)方式并不是線程安全的:
- /**
- * Returns the single instance of this class, creating it if necessary.
- *
- * @param tasksRemoteDataSource the backend data source
- * @param tasksLocalDataSource the device storage data source
- * @return the {@link TasksRepository} instance
- */
- public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
- TasksDataSource tasksLocalDataSource) {
- if (INSTANCE == null) {
- INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
- }
- return INSTANCE;
- }
說到底,他根本沒有線程安全的必要,至少在這個app里,沒有并發(fā)創(chuàng)建這個對象的場景,所以夠用就行了。在TR內(nèi)部使用了一個LinkedHashMap作為容器來保存Tasks,主要看一下兩個方法,首先是存儲:
- public void saveTask(@NonNull Task task) {
- checkNotNull(task);
- mTasksRemoteDataSource.saveTask(task);
- mTasksLocalDataSource.saveTask(task);
- // Do in memory cache update to keep the app UI up to date
- if (mCachedTasks == null) {
- mCachedTasks = new LinkedHashMap<>();
- }
- mCachedTasks.put(task.getId(), task);
- }
會將傳入的task存儲到遠(yuǎn)程數(shù)據(jù)源和本地數(shù)據(jù)源(本地數(shù)據(jù)庫)中,然后將這個task傳到mCachedTasks(LinkedHashMap)中。代碼比較簡單,不做更多的分析,接下來看一下讀取Task:
- 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);
- }
- });
- }
- }
這個taskId是需要獲取Task的id,也是唯一標(biāo)識,GetTaskCallback則是負(fù)責(zé)傳遞數(shù)據(jù)的接口回調(diào)。首先是從內(nèi)存中讀取數(shù)據(jù),getTaskWithId方法就是,看一下代碼:
- private Task getTaskWithId(@NonNull String id) {
- checkNotNull(id);
- if (mCachedTasks == null || mCachedTasks.isEmpty()) {
- return null;
- } else {
- return mCachedTasks.get(id);
- }
- }
就從保存task的LinkedHashMap中讀取數(shù)據(jù)。如果這個過程讀取不到數(shù)據(jù)那么接著從本地數(shù)據(jù)源中讀取數(shù)據(jù),如果本地數(shù)據(jù)源也沒有拿到這個數(shù)據(jù),那么最終就從遠(yuǎn)程數(shù)據(jù)源中讀取數(shù)據(jù)。
至此,我們簡單的過了一遍這個項(xiàng)目。
總結(jié) & 再談MVP
Google這個示例項(xiàng)目,架構(gòu)非常的清晰,也是很標(biāo)準(zhǔn)的MVP模式,項(xiàng)目中解耦做的也非常好。但是相對于一個功能簡單的應(yīng)用來說,代碼量還是比較多的。當(dāng)然,因?yàn)檫@只是一個小例子而已,可能會讓人覺得反而不如普通的MVC來開發(fā)方便,但是人無遠(yuǎn)慮必有近憂。我們做東西的時候要盡量做長遠(yuǎn)的打算,不然以后可能就會被淹沒在頻繁的需求變更里了。Google的這個項(xiàng)目有非常多值得我們學(xué)習(xí)的地方,比如我們寫MVP的時候也可以用一個Contract類來將View和Presenter放入其中,方便我們管理(改代碼)。
我們都知道MVP與MVC的主要區(qū)別是View和Model不直接交互,而是通過Presenter來完成交互,這樣可以修改View而不影響Model,實(shí)現(xiàn)了Model和View真正的完全分離。而MVP中將業(yè)務(wù)邏輯抽取放到Presenter中,使各個模塊的職責(zé)更加清晰,層次明了。而且還有很關(guān)鍵的一點(diǎn),使用MVP架構(gòu)使得應(yīng)用能更加方便的進(jìn)行單元測試。Android中雖然有很多測試框架,但是講實(shí)話,你不研究個一段時間很難使用那些框架進(jìn)行有效的測試。而且很多測試是難以進(jìn)行的,因?yàn)橛械男枰蕾嘇ndroid環(huán)境或者UI環(huán)境。而如果使用了MVP架構(gòu),View層因?yàn)槭怯媒涌诙x的,所以完全可以自己建一個View模擬視圖對象,這樣就可以使得我們的測試不必依賴UI環(huán)境。這樣***的好處就是我們不必花費(fèi)太多的時間去研究那些測試框架,也能寫出有效的單元測試,保證我們代碼的質(zhì)量。
相較于MVP的優(yōu)點(diǎn),其缺點(diǎn)也是非常明顯的,從Google的這個示例代碼也能看出來,代碼量比較大,小型Android應(yīng)用的開發(fā)用這個反而麻煩。Presenter既負(fù)責(zé)業(yè)務(wù)邏輯,又負(fù)責(zé)Model和View的交互,到后期也難免會膨脹、臃腫,最終造成這玩意可能維護(hù)起來也不簡單。
雖然MVP還是有不足的地方,但是相較于MVC,還是更容易的寫出易維護(hù)、測試的代碼的,所以各位不妨都閱讀一下Google的這個代碼~