Google官方MVP示例程式碼閱讀筆記
寫在前面
這個專案很久之前就從** android-architecture ** 這個倉庫clone了這個MVP架構的todoapp,原始碼也讀過,不過沒有整理過。最近整理資料準備畢設了,再讀一遍原始碼,感受和以前又不同了。先放上專案地址,各位可以自己去clone或者下載:https://github.com/googlesamples/android-architecture/tree/todo-mvp
如果各位對MVP模式不是很熟悉,可以看我之前的一篇文:
Android之MVP初嘗試,簡單易懂。下文的view一般是指MVP中的view。
剝絲抽繭,理清專案結構
國際慣例,上專案結構圖:
從包名上很容易分辨出功能:addedittask是新增任務,data是資料管理,statistics是統計,taskdetail是任務詳情,tasks是任務瀏覽之類的。事實上這個專案的關鍵也就是:** Tasks 、 TaskDetail 、 AddEditTask 、 Statistics **。
這四個關鍵的地方都有相同之處:
- 定義了view和presenter的契約
- Activity負責fragment和presenter的建立
- Fragment實現了view介面
- presenter實現了presenter介面
也就是說,幾個功能每一個都是MVP的模式,只不過Model層是公用的。而且這個專案裡View層都是Fragment,果然google推薦用Fragment自己的專案裡也給我們做個示範……其實關於到底是不是要用Fragment,還是有些爭議的,我為什麼不主張使用Fragment,這篇文關於Fragment講的比較到位了。那麼到底要不要用呢?我覺得對於個體而言,不管你喜不喜歡,都要用一用,試一試,因為人要成長,必須踩坑。對於正式專案而言,則需要綜合考量,使用Fragment的利是否大於弊。
扯遠了,接下來看一下他程式碼倉庫給的一張結構圖:
可以看出來左邊是資料管理,典型的Model層。而右邊呢,你可能認為Activity是Presenter,事實上並不是,Presenter在Activity內,Fragment是View無疑。到這,我覺得關於這個專案結構的簡介已經足夠了,接下來看程式碼。
我覺得看一個Android專案的正確姿勢應該是先把玩一下app,看一下功能。貼幾張app的圖:
接著就該上入口的Activity看一下了,這個專案的入口Activity是TasksActivity,所在的包是tasks,看一下有哪些東西:
第一個是自定義View,第二個就是入口Activity了,第三個即上面所說的“契約”,裡面包含了View介面和Presenter介面。TasksFilterType則是一個列舉,裡面有三個過濾型別:所有,進行中的,完成的。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裡設定的點選事件
TasksFragment tasksFragment =
(TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
// getSupportFragmentManager().findFragmentById()
if (tasksFragment == null) {
// Create the fragment
tasksFragment = TasksFragment.newInstance();
// 提供方法幫助activity載入ui
// 這個方法其實就是拿到一個事務,然後把這個fragment add到對應的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和側滑,這裡不必深入細節,可以跳過這倆。之後初始化fragment和presenter,初始化Fragment先是嘗試通過id尋找可能已經存在的Fragment物件,如果沒有,則重新建立一個Fragment物件。下一步則是建立一個presenter,最後則是讓應用在橫豎屏狀態切換的情況下恢復資料。
接下來看一下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裡的方法比較多,事實上這是應該的。因為在MVP架構裡,View只負責根據Presenter的指示繪製UI,View將所有的使用者互動交給Presenter處理。所以Presenter的很多方法可能就是對使用者的輸入的處理,而有輸入必然有輸出,View介面定義的各個方法便是給Presenter回撥的。Presenter通過回撥函式將對使用者的輸入的處理結果推到View中,View再根據這個結果對UI進行相應的更新。而在此專案中,Fragment就是View,在Fragment的各個點選事件中都呼叫了Presenter的對應方法,將業務邏輯交給Presenter處理。這看起來比傳統的MVC強上很多,因為傳統MVC中Activity既可以認為是Controller亦可以認為是View,職責難以分離,寫到後面可能一個Activity就有上千行的程式碼,這會為後續的維護帶來不少麻煩。而MVP則將業務邏輯抽取到了Presenter中,作為View的Fragment或者Activity職責更加單一,無疑為後續的開發維護帶來了便利。
接下來詳細的看Presenter的初始化,Presenter的建立是在TasksActivity中完成的,檢視其建構函式:
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內的引用,呼叫view的setPresenter方法,將自身傳入,這樣view中就可以使用presenter物件了,比直接從activity中拿看起來要優雅了不少。Presenter具體的邏輯就不看了,都是一些比較簡單的程式碼,回顧一下開啟這個app所發生的事件的流程:建立TasksActivity -> 初始化Toolbar -> 初始化側滑 -> 建立TasksFragment物件 -> 建立TaskPresenter物件 -> 給Fragment設定Presenter物件 -> 初始化Fragment佈局,這樣一套流程下來,整個流程就理清了,接下來只是等待使用者的輸入了。
接下來要看的是從本文開始到現在都一直忽略了的Model:TasksRepository。不過在分析TasksRepository之前,安利一下這個專案裡的實體類,寫的比較優雅,我們平時寫實體類時最好也能按照他的套路來寫。我為什麼說他寫的比較優雅呢?因為各個屬性或者是帶返回值的方法都打上了@Nullable或者@NoNull註解來說明是否可以為空,事實上空指標這個錯可以算是平時經常遇到的錯了……不過如果你有良好的設計和編碼習慣,是可以避免的,帶上這兩個註解可以在編譯期給你相關的提示。不僅如此,這個實體類還複寫了equals()、hashCode()和toString()方法,而且實現的方式也符合規範,關於如何複寫這三個方法,在《effective java》上有很好的總結,各位可以去讀一下。
/*
* 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所在的包的結構:
可以從包名上看出local是從本地讀取資料,remote是遠端讀取,當然了,這裡只是模擬遠端讀取。本地採用了資料庫存取的方式。在TasksRepository(下文簡稱TR)內部有兩個TasksDataSource的引用:
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
TasksDataSource是data包內的一個介面,使用介面引用,無非是想解耦,就算以後需求變更,不想採用資料庫的方式儲存資料,只要實現了這個介面,TR內部的程式碼也無需變更。TR用了單例,實現方式並不是執行緒安全的:
/**
* 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裡,沒有併發建立這個物件的場景,所以夠用就行了。在TR內部使用了一個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儲存到遠端資料來源和本地資料來源(本地資料庫)中,然後將這個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,也是唯一標識,GetTaskCallback則是負責傳遞資料的介面回撥。首先是從記憶體中讀取資料,getTaskWithId方法就是,看一下程式碼:
private Task getTaskWithId(@NonNull String id) {
checkNotNull(id);
if (mCachedTasks == null || mCachedTasks.isEmpty()) {
return null;
} else {
return mCachedTasks.get(id);
}
}
就從儲存task的LinkedHashMap中讀取資料。如果這個過程讀取不到資料那麼接著從本地資料來源中讀取資料,如果本地資料來源也沒有拿到這個資料,那麼最終就從遠端資料來源中讀取資料。
至此,我們簡單的過了一遍這個專案。
總結 & 再談MVP
Google這個示例專案,架構非常的清晰,也是很標準的MVP模式,專案中解耦做的也非常好。但是相對於一個功能簡單的應用來說,程式碼量還是比較多的。當然,因為這只是一個小例子而已,可能會讓人覺得反而不如普通的MVC來開發方便,但是人無遠慮必有近憂。我們做東西的時候要儘量做長遠的打算,不然以後可能就會被淹沒在頻繁的需求變更裡了。Google的這個專案有非常多值得我們學習的地方,比如我們寫MVP的時候也可以用一個Contract類來將View和Presenter放入其中,方便我們管理(改程式碼)。
我們都知道MVP與MVC的主要區別是View和Model不直接互動,而是通過Presenter來完成互動,這樣可以修改View而不影響Model,實現了Model和View真正的完全分離。而MVP中將業務邏輯抽取放到Presenter中,使各個模組的職責更加清晰,層次明瞭。而且還有很關鍵的一點,使用MVP架構使得應用能更加方便的進行單元測試。Android中雖然有很多測試框架,但是講實話,你不研究個一段時間很難使用那些框架進行有效的測試。而且很多測試是難以進行的,因為有的需要依賴Android環境或者UI環境。而如果使用了MVP架構,View層因為是用介面定義的,所以完全可以自己建一個View模擬檢視物件,這樣就可以使得我們的測試不必依賴UI環境。這樣最大的好處就是我們不必花費太多的時間去研究那些測試框架,也能寫出有效的單元測試,保證我們程式碼的質量。
相較於MVP的優點,其缺點也是非常明顯的,從Google的這個示例程式碼也能看出來,程式碼量比較大,小型Android應用的開發用這個反而麻煩。Presenter既負責業務邏輯,又負責Model和View的互動,到後期也難免會膨脹、臃腫,最終造成這玩意可能維護起來也不簡單。
雖然MVP還是有不足的地方,但是相較於MVC,還是更容易的寫出易維護、測試的程式碼的,所以各位不妨都閱讀一下Google的這個程式碼~
相關文章
- 程式碼大全2閱讀筆記筆記
- 《程式碼大全》閱讀筆記1(2024.10.4)筆記
- 05夢斷程式碼閱讀筆記筆記
- 03夢斷程式碼閱讀筆記筆記
- 04夢斷程式碼閱讀筆記筆記
- 《程式碼大全2》閱讀筆記01筆記
- C++程式碼閱讀筆記(一)筆記
- 夢斷程式碼閱讀筆記之六筆記
- 從google todo-mvp示例再次學習MVPGoMVP
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- python原始碼閱讀筆記Python原始碼筆記
- CopyOnWriteArrayList原始碼閱讀筆記原始碼筆記
- ArrayList原始碼閱讀筆記原始碼筆記
- LinkedList原始碼閱讀筆記原始碼筆記
- guavacache原始碼閱讀筆記Guava原始碼筆記
- LongAdder原始碼閱讀筆記原始碼筆記
- Koa 原始碼閱讀筆記原始碼筆記
- JDK原始碼閱讀(4):HashMap類閱讀筆記JDK原始碼HashMap筆記
- JDK原始碼閱讀(5):HashTable類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀(7):ConcurrentHashMap類閱讀筆記JDK原始碼HashMap筆記
- Express Session 原始碼閱讀筆記ExpressSession原始碼筆記
- pytorch程式碼示例筆記 -- AutogradPyTorch筆記
- 閱讀筆記筆記
- JDK原始碼閱讀(3):AbstractStringBuilder、StringBuffer、StringBuilder類閱讀筆記JDK原始碼UI筆記
- 【閱讀筆記】Taro轉小程式編譯原始碼解析筆記編譯原始碼
- goroutine排程原始碼閱讀筆記Go原始碼筆記
- 【iOS印象】GLPubSub 原始碼閱讀筆記iOS原始碼筆記
- Flask 原始碼閱讀筆記 開篇Flask原始碼筆記
- Redux 學習筆記 – 原始碼閱讀Redux筆記原始碼
- Android Google MVP Demo TODO解讀AndroidGoMVP
- 閱讀筆記2筆記
- 閱讀筆記1筆記
- gdbOF閱讀筆記筆記
- 閱讀筆記5筆記
- 閱讀筆記8筆記
- 閱讀筆記7筆記
- 閱讀筆記03筆記
- 閱讀筆記02筆記