GOOGLE TODO-MVP 學(xué)習(xí)筆記
背景(可忽略):《GOOGLE TODO-MVP 學(xué)習(xí)筆記》這篇文章主要會記錄自己在根據(jù)TODO-MVP這個項目學(xué)習(xí)MVP的過程中的一些心得和想法难裆,一是為了自己記錄下來命锄,二是為了說出來脐恩,增強自己的理解驶冒。
由于時間及經(jīng)驗有限骗污,文中可能存在錯誤與不足需忿,歡迎大家指出屋厘,我會第一時間對文章進行修改糾正。
如果對MVP模式不是很了解的溢谤,可以先去看看相關(guān)文章世杀,這里推薦diygreen的兩篇文章瞻坝,MVP詳解上下。
google 項目地址:https://github.com/googlesamples/android-architecture/
選擇不同的分支包晰,本文的是TODO-MVP湿镀,也是最基礎(chǔ)的沥阱。
本文主要講了兩個部分
- 在TODO-MVP中是如何實現(xiàn)MVP的
- 一個簡單的單元測試
TODO-MVP
先來一個整體的概覽:
整個項目結(jié)構(gòu)特別清晰赋咽,最外層是五個文件夾般眉,兩個代碼目錄,三個測試目錄蒸矛,之前看文章有說四個測試目錄的瀑罗,不過個人不是很認同。其中在main文件夾下是我們主要的代碼(找不到的請切換到Project結(jié)構(gòu))雏掠,展開的部分就是斩祭,可以看到是按照業(yè)務(wù)模塊劃分的,從上到下依次是添加模塊乡话,數(shù)據(jù)層摧玫,統(tǒng)計模塊,詳細模塊绑青,展示模塊诬像,工具類,PV基類闸婴,名字起的有些隨意坏挠,再看每一個包中的具體類,以tasks為例:
- ScrollChildSwipeRefreshLayout----自定義View
- TasksActivity-----------------------------負責(zé)創(chuàng)建V,P
- TasksContract---------------------------接口邪乍,V,P接口的紐帶
- TasksFilterType-------------------------枚舉類
- TasksFragment-------------------------View層實現(xiàn)類
- TasksPresenter-------------------------Presenter層實現(xiàn)類
先以代碼的方式了解下View層和Presenter層是如果創(chuàng)建并工作的降狠,先來看看Activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tasks_act);
//UI相關(guān)初始化,忽略
//通過工具類創(chuàng)建一個Fragment庇楞,是View層的實現(xiàn)類
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
// 創(chuàng)建一個Presenter
mTasksPresenter = new TasksPresenter(
Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
// Load previously saved state, if available.
//恢復(fù)界面中Task的類別
if (savedInstanceState != null) {
TasksFilterType currentFiltering =
(TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
}
}
可以看到在Activity初始化的時候分別創(chuàng)建了一個Fragment(View的實現(xiàn)類)榜配,一個TasksPresenter(Presenter的實現(xiàn)類),注意Presenter在構(gòu)建的時候需要傳入一個View對象姐刁。接下來看看Presenter初始化的時候都做了些什么:
<-- Injection -- >
public class Injection {
public static TasksRepository provideTasksRepository(@NonNull Context context) {
checkNotNull(context);
return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
TasksLocalDataSource.getInstance(context));
}
}
<-- Taskspresenter -- >
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!");
//為View層設(shè)置對應(yīng)的Presenter層對象
mTasksView.setPresenter(this);
}
先通過Injection的靜態(tài)方法provideTasksRepository()創(chuàng)建一個TasksRepository(Model層的實現(xiàn)類)芥牌,之后將其與Fragment通過構(gòu)造函數(shù)傳遞到Presenter中烦味,這樣在P層初始化的時候就持有了M和V的對象聂使。之后會通過View.setPresenter(P)方法為View層設(shè)置對應(yīng)的Presenter∶恚看一下Fragment中的代碼:
<-- BaseView -- >
public interface BaseView<T> {
void setPresenter(T presenter);
}
<-- TasksFragment -- >
private TasksContract.Presenter mPresenter;
@Override
public void setPresenter(@NonNull TasksContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
先在View的基類中聲明抽象的設(shè)置方法柏靶,然后在Presenter初始化的時候?qū)resenter注入到View中。
總結(jié)一下:
- 在Activity創(chuàng)建的時候創(chuàng)建一個View對象溃论,一個Presenter對象屎蜓。
- 在創(chuàng)建presenter的時候?qū)⒁粋€Model,上一步中創(chuàng)建好的View钥勋,通過構(gòu)造函數(shù)注入到Presenter中炬转。
- 在Presenter的構(gòu)造方法中辆苔,通過View.setPresenter(P)方法,將Presenter設(shè)置到View中扼劈。
接下來用一個簡單的例子來走一遍整體流程驻啤,以添加一個loadTasks為例:
第一步: 在TasksFragment的onResume()方法中,Presenter層開始工作荐吵。
<-- TasksFragment -->
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
第二步: TasksPresenter.start()方法中調(diào)用了loadTasks()方法骑冗,我們需要在TasksContract.Presenter中去規(guī)定這個方法,然后再在TasksPresenter中去實現(xiàn)它先煎。
<-- TasksPresenter -->
@Override
public void start() {
loadTasks(false);
}
<-- TasksContract.Presenter -->
void loadTasks(boolean forceUpdate);
<-- TasksPresenter -->
private boolean mFirstLoad = true;
@Override
public void loadTasks(boolean forceUpdate) {
// Simplification for sample: a network reload will be forced on first load.
loadTasks(forceUpdate || mFirstLoad, true);
mFirstLoad = false;
}
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
//調(diào)用TasksRepository.getTasks方法去獲取數(shù)據(jù)
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
//篩選想要類型的Task
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);
}
@Override
public void onDataNotAvailable() {
// The view may not be able to handle UI updates anymore
if (!mTasksView.isActive()) {
return;
}
mTasksView.showLoadingTasksError();
}
});
}
第三步:調(diào)用TasksRepository.getTasks()方法贼涩,所以需要在TasksDataSource中添加getTasks,然后讓TasksRepository去實現(xiàn)這個方法薯蝎,在這個方法中調(diào)用具體的數(shù)據(jù)層的實現(xiàn)類mTasksRemoteDataSource遥倦,mTasksLocalDataSource中的getTasks,之后通過傳遞過來的接口將數(shù)據(jù)返回到Presenter中占锯。
<-- TasksRepository -->
//具體的遠程數(shù)據(jù)實現(xiàn)類
private final TasksDataSource mTasksRemoteDataSource;
//具體的本地數(shù)據(jù)實現(xiàn)類
private final TasksDataSource mTasksLocalDataSource;
//內(nèi)存緩存
Map<String, Task> mCachedTasks;
@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);
}
});
}
}
第四步:調(diào)用mTasksView.showTasks()展示數(shù)據(jù)谊迄,所以需要在TasksContract.View中定義方法 void showTasks(List<Task> tasks);然后在TasksFragment中去實現(xiàn)。
<-- TasksPresneter -->
private void processTasks(List<Task> tasks) {
if (tasks.isEmpty()) {
// Show a message indicating there are no tasks for that filter type.
processEmptyTasks();
} else {
// Show the list of tasks
//顯示查詢回來的數(shù)據(jù)
mTasksView.showTasks(tasks);
// Set the filter label's text.
showFilterLabel();
}
}
<-- TasksFragment -->
@Override
public void showTasks(List<Task> tasks) {
mListAdapter.replaceData(tasks);
mTasksView.setVisibility(View.VISIBLE);
mNoTasksView.setVisibility(View.GONE);
}
這樣一個流程就跑通了烟央,從在View中調(diào)用Presenter方法去請求數(shù)據(jù)统诺,Presenter中調(diào)用Model方法去獲取數(shù)據(jù),Model在調(diào)用具體實現(xiàn)方法疑俭,獲取數(shù)據(jù)之后粮呢,將數(shù)據(jù)通過接口返回到Presenter中,之后再調(diào)用View的方法展示數(shù)據(jù)钞艇。
但是啄寡,如果是按照上面的順序去寫代碼的話,肯定會覺得這實在是太復(fù)雜了哩照,多寫好多東西挺物,所以個人猜測應(yīng)該不是按照上面的方式去寫的,猜測應(yīng)該是這樣:
第一步:在TasksContract.Presenter中去寫一個方法讓它去加載數(shù)據(jù)飘弧,比如LoadTasks()识藤;
第二步:在TasksDataSource中寫一個方法讓它去獲取數(shù)據(jù),比如getTasks()次伶,之后再定義一個接口痴昧,用于傳遞數(shù)據(jù),抽象一個方法參數(shù)是Task集合冠王,一個簡單的接口回調(diào)赶撰。
第三步:在TasksContract.View中寫一個方法,用于展示數(shù)據(jù),參數(shù)肯定是要展示的數(shù)據(jù)了豪娜,比如showTasks(tasks)餐胀;
第四步:寫各自的實現(xiàn)方法。在View中不用考慮數(shù)據(jù)是怎么來的瘤载,只管UI的變化就好骂澄;在Presenter中不用管怎么展示,怎么獲取數(shù)據(jù)惕虑,只管應(yīng)該找誰要數(shù)據(jù)坟冲,之后處理一下,交給View去顯示就好了溃蔫;在Model中健提,只需要得到數(shù)據(jù),傳遞給Presenter就可以了伟叛,其他的完全不用操心私痹。
在每一個自己的層級中做自己應(yīng)該做的事情,并且對其他的東西盡量少的了解统刮,盡可能的不出現(xiàn)干涉紊遵,專注做自己的事情,這樣代碼寫起來其實會清晰很多侥蒙,更加富有條理暗膜,而且在以后的擴展或者修改會變得更加的容易,而不會有那種牽一發(fā)而動全身的感覺鞭衩。
不知道各位對上面第二種寫代碼的方法覺得怎么樣学搜,個人認為,當(dāng)接口方法確定了之后论衍,其實整個開發(fā)工作基本上就完成百分之七十了瑞佩,在View中不用去考慮業(yè)務(wù)邏輯,不用去考慮UI的變化坯台,因為數(shù)據(jù)傳遞過來之后所有的事情就都已經(jīng)確定了炬丸,在Presenter中不用去考慮數(shù)據(jù)的來源,在Model中不去考慮數(shù)據(jù)的預(yù)處理和變換蜒蕾,將所有需要做的功能或者是動作都盡可能的細化稠炬,細化到每一層的每一個方法中,在一個方法中只做一件事情滥搭,其他的并不知道酸纲,也不需要知道捣鲸,剩下的工作就是簡單的填充代碼了瑟匆。
一個簡單的單元測試
其實在TODO-MVP中測試的代碼要比正式的代碼要多,雖然沒有具體數(shù)過,不過從目錄數(shù)量來看就已經(jīng)證明了一點愁溜,測試真的很重要疾嗅,我之前從來沒有寫過任何測試代碼,也沒有專門學(xué)過冕象,只是在平時看了幾篇測試相關(guān)的文章代承,太深的講不了,太淺的說著也沒意思渐扮,就拿一個例子來說论悴,當(dāng)然,在說具體的例子之前墓律,如果各位對相關(guān)的單元測試的知識不是很了解的話膀估,推薦大家?guī)灼恼? 。
鄒小創(chuàng)耻讽,相關(guān)測試文章十一篇察纯,由淺入深,通俗易懂针肥。
鍵盤男饼记,介紹一些實際測試中的經(jīng)驗。
單元測試?yán)?Mockito 中文文檔介紹Mockito相關(guān)API和使用方法慰枕,很全面具则。
看過以上大神的文章之后,自己再隨便瀏覽一些相關(guān)文章具帮,基本上就沒問題了乡洼。
接下來就是例子了,代碼是在test文件夾下tasks包中的TasksPresenter類
<--TasksPresenterTest>
private static List<Task> TASKS;
@Mock
private TasksRepository mTasksRepository;
@Mock
private TasksContract.View mTasksView;
/**
* {@link ArgumentCaptor} is a powerful Mockito API to capture argument values and use them to
* perform further actions or assertions on them.
*/
@Captor
private ArgumentCaptor<LoadTasksCallback> mLoadTasksCallbackCaptor;
private TasksPresenter mTasksPresenter;
@Before
public void setupTasksPresenter() {
// Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
// inject the mocks in the test the initMocks method needs to be called.
MockitoAnnotations.initMocks(this);
// Get a reference to the class under test
mTasksPresenter = new TasksPresenter(mTasksRepository, mTasksView);
// The presenter won't update the view unless it's active.
when(mTasksView.isActive()).thenReturn(true);
// We start the tasks to 3, with one active and two completed
TASKS = Lists.newArrayList(new Task("Title1", "Description1"),
new Task("Title2", "Description2", true), new Task("Title3", "Description3", true));
}
如果上面的代碼匕坯,看不明白束昵,那還是去閱讀我剛才推薦的文章,這里就簡單的說一下葛峻,先是mock了兩個對象,相關(guān)的model和view锹雏。通過@Before
注解,在所有的測試方法做初始化术奖,依據(jù)mock的對象礁遵,創(chuàng)建一個presenter,設(shè)置測試樁采记,初始化數(shù)據(jù)佣耐。
接下來只看一個方法:
<--TasksPresenterTest>
@Test
public void loadAllTasksFromRepositoryAndLoadIntoView() {
// Given an initialized TasksPresenter with initialized tasks
// When loading of Tasks is requested
//提供一個篩選的Task類型
mTasksPresenter.setFiltering(TasksFilterType.ALL_TASKS);
//加載數(shù)據(jù)
mTasksPresenter.loadTasks(true);
// Callback is captured and invoked with stubbed tasks
//驗證model的加載數(shù)據(jù)的方法是否執(zhí)行,并且對入?yún)⑦M行捕獲
verify(mTasksRepository).getTasks(mLoadTasksCallbackCaptor.capture());
//設(shè)置接口回調(diào)傳遞回來的數(shù)據(jù)
mLoadTasksCallbackCaptor.getValue().onTasksLoaded(TASKS);
// Then progress indicator is shown
//驗證mock view的方法執(zhí)行順序唧龄,創(chuàng)建一個inorder對象
InOrder inOrder = inOrder(mTasksView);
//驗證view.setLoadingIndicator(true)是否執(zhí)行
inOrder.verify(mTasksView).setLoadingIndicator(true);
// Then progress indicator is hidden and all tasks are shown in UI
//驗證view.setLoadingIndicator(false)是否執(zhí)行
inOrder.verify(mTasksView).setLoadingIndicator(false);
//創(chuàng)建一個參數(shù)捕獲器
ArgumentCaptor<List> showTasksArgumentCaptor = ArgumentCaptor.forClass(List.class);
//驗證view.showTasks()方法是否執(zhí)行兼砖,并且執(zhí)行對其參數(shù)進行捕獲
verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
//斷言 判斷捕獲的參數(shù)也就是傳入showTasks方法中的list的size是否為3
assertTrue(showTasksArgumentCaptor.getValue().size() == 3);
}
代碼中對每一句都進行了注釋,很好理解,這里對參數(shù)捕獲器說一下讽挟,最開始我不明白這個東西是個什么玩意懒叛,怎么工作的,網(wǎng)上查就說是參數(shù)捕獲器耽梅,能夠捕捉到一個方法的入?yún)⒌南嚓P(guān)信息薛窥。然后自己就照著demo寫,寫完發(fā)現(xiàn)一運行報錯了眼姐,如果是驗證方法诅迷, 或者斷音之類的問題,會有相關(guān)提示的众旗,我這報錯沒有啊竟贯。
我還以為是為代碼寫的有問題,就把google里的代碼拷過來逝钥,運行屑那,還是不行,這就奇怪了艘款,為什么同樣的的代碼在別人那就沒問題持际,在我這就報錯那,之后就開始排查問題哗咆,先一句句從下往上注釋掉蜘欲,運行,看看是哪句出的問題晌柬,被我發(fā)現(xiàn)了是這句verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
姥份,這我就更不明白了,這個是展示數(shù)據(jù)的年碘,肯定沒問題的啊澈歉,然后就去原代碼中去排查,從上到下看了一遍屿衅,沒問題埃难,又看了一遍沒問題,然后又回到測試代碼涤久,各種改涡尘,想看看是哪的問題,就上面的那幾句測試代碼响迂,我玩了半天考抄,還是沒有找到為什么,不行了蔗彤,估計是自己對mock這個東西不是很了解川梅,就去查網(wǎng)上的資料疯兼,看各種譯文,實例文章挑势,介紹文章镇防。當(dāng)看到下面內(nèi)容的時候我好想似乎明白了些什么啦鸣。
是不是view.showTasks()方法沒有執(zhí)行啊潮饱,那樣的話參數(shù)就不會被捕獲,所以就去之前正式代碼中添加打印語句诫给,發(fā)現(xiàn)香拉,確實沒有執(zhí)行,為什么沒有執(zhí)行那中狂,繼續(xù)往上找凫碌,在數(shù)據(jù)遍歷的時候:
我擦嘞,我居然把集合寫錯了胃榕,可能是敲的時候沒注意直接就確定了盛险,也沒看是哪一個了,改過來之后再運行勋又,總終于成功了苦掘,就這么個問題,搞了我一天半楔壤,不過經(jīng)歷了這么個事情之后鹤啡,我發(fā)現(xiàn)對于這些基本的測試樁,驗證蹲嚣,斷言递瑰,順序執(zhí)行,熟悉的不要不要的隙畜,真是沒有磨難就沒有進步啊抖部。
這樣一個簡單的單元測試就完成了,測試內(nèi)容那就是presenter加載數(shù)據(jù)议惰,驗證model獲取數(shù)據(jù)您朽,設(shè)置接口回調(diào)參數(shù),驗證view方法執(zhí)行順序换淆,驗證view方法是否執(zhí)行哗总,捕獲參數(shù),比對參數(shù)內(nèi)數(shù)據(jù)倍试。
多說幾句
說一下我在學(xué)習(xí)這個項目的一些心得體會吧:
- 代碼不是看的讯屈,一定要敲。不知道大家怎么去學(xué)習(xí)別人的項目县习,在我看來涮母,最好的學(xué)習(xí)方式谆趾,就是把別人的項目敲一遍,看的時候有可能不過腦叛本,敲的時候就肯定得思考了沪蓬,為什么這么分包,應(yīng)該怎么調(diào)用来候,之類的跷叉。
- 什么東西都不要浮于表面。因為現(xiàn)在已經(jīng)不是那個营搅,我見過云挟,我了解的時代了。要盡可能的做到转质,我知道园欣,我熟悉,我敲過休蟹,我寫過相關(guān)案例沸枯, 我看過源碼,我了解底層實現(xiàn)赂弓。
- 關(guān)于這個項目還有一部分沒有介紹绑榴,那就是UI測試,目前正在看資料拣展,之后應(yīng)該會出一篇彭沼,不過應(yīng)該是在整個項目都敲完的時候了。其實個人感覺备埃,單元測試這個東西姓惑,重在經(jīng)驗,入門其實很容易按脚,五六個注解于毙,三四個方法,打樁辅搬,驗證唯沮,斷言,任何一個人估計半天到一天應(yīng)該都差不多堪遂,感覺更重要的是正式代碼的書寫介蛉,如果正式代碼寫的不好,測試代碼寫都不能寫溶褪,更別說驗證了币旧,而且測試經(jīng)驗很重要,只有經(jīng)歷足夠多的測試案例猿妈,才會真正掌握單元測試的精髓吧吹菱,所謂的測試驅(qū)動開發(fā)巍虫,想想就覺得好激動。
- 我這才剛?cè)腴T鳍刷,還差得遠那占遥。