什么是android mvp模式
關(guān)于Android程序的構(gòu)架, 當(dāng)前最流行的模式即為MVP模式, Google官方提供了Sample代碼來展示這種模式的用法。
Repo地址: android-architecture.
本文為閱讀官方sample代碼的閱讀筆記和分析。
官方Android Architecture Blueprints [beta]:
Android在如何組織和構(gòu)架一個(gè)app方面提供了很大的靈活性, 但是同時(shí)這種自由也可能會(huì)導(dǎo)致app在測(cè)試, 維護(hù), 擴(kuò)展方面變得困難。
Android Architecture Blueprints展示了可能的解決方案。 在這個(gè)項(xiàng)目里, 我們用各種不同的構(gòu)架概念和工具實(shí)現(xiàn)了同一個(gè)應(yīng)用(To Do App)。 主要的關(guān)注點(diǎn)在于代碼結(jié)構(gòu), 構(gòu)架, 測(cè)試和維護(hù)性。
但是請(qǐng)記住, 用這些模式構(gòu)架app的方式有很多種, 要根據(jù)你的需要, 不要把這些當(dāng)做絕對(duì)的典范。
MVP模式 概念
之前有一個(gè)MVC模式: Model-View-Controller.
MVC模式 有兩個(gè)主要的缺點(diǎn): 首先, View持有Controller和Model的引用; 第二, 它沒有把對(duì)UI邏輯的操作限制在單一的類里, 這個(gè)職能被Controller和View或者M(jìn)odel共享。
所以后來提出了MVP模式來克服這些缺點(diǎn)。
MVP(Model-View-Presenter)模式:
Model: 數(shù)據(jù)層。 負(fù)責(zé)與網(wǎng)絡(luò)層和數(shù)據(jù)庫層的邏輯交互。
View: UI層。 顯示數(shù)據(jù), 并向Presenter報(bào)告用戶行為。
Presenter: 從Model拿數(shù)據(jù), 應(yīng)用到UI層, 管理UI的狀態(tài), 決定要顯示什么, 響應(yīng)用戶的行為。
MVP模式的最主要優(yōu)勢(shì)就是耦合降低, Presenter變?yōu)榧?a href="http://www.xsypw.cn/v/tag/852/" target="_blank">Java的代碼邏輯, 不再與Android Framework中的類如Activity, Fragment等關(guān)聯(lián), 便于寫單元測(cè)試。
todo-mvp 基本的Model-View-Presenter架構(gòu)
app中有四個(gè)功能:
Tasks
TaskDetail
AddEditTask
Statistics
每個(gè)功能都有:
一個(gè)定義View和Presenter接口的Contract接口;
一個(gè)Activity用來管理fragment和presenter的創(chuàng)建;
一個(gè)實(shí)現(xiàn)了View接口的Fragment;
一個(gè)實(shí)現(xiàn)了Presenter接口的presenter.
mvp
基類
Presenter基類:
public interface BasePresenter {
void start();
}
例子中這個(gè)start()方法都在Fragment的onResume()中調(diào)用。
View基類:
public interface BaseView《T》 {
void setPresenter(T presenter);
}
View實(shí)現(xiàn)
Fragment作為每一個(gè)View接口的實(shí)現(xiàn), 主要負(fù)責(zé)數(shù)據(jù)顯示和在用戶交互時(shí)調(diào)用Presenter, 但是例子代碼中也是有一些直接操作的部分, 比如點(diǎn)擊開啟另一個(gè)Activity, 點(diǎn)擊彈出菜單(菜單項(xiàng)的點(diǎn)擊仍然是調(diào)用presenter的方法)。
View接口中定義的方法多為showXXX()方法。
Fragment作為View實(shí)現(xiàn), 接口中定義了方法:
@Override
public boolean isActive() {
return isAdded();
}
在Presenter中數(shù)據(jù)回調(diào)的方法中, 先檢查View.isActive()是否為true, 來保證對(duì)Fragment的操作安全。
Presenter實(shí)現(xiàn)
Presenter的start()方法在onResume()的時(shí)候調(diào)用, 這時(shí)候取初始數(shù)據(jù); 其他方法均對(duì)應(yīng)于用戶在UI上的交互操作。
New Presenter的操作是在每一個(gè)Activity的onCreate()里做的: 先添加了Fragment(View), 然后把它作為參數(shù)傳給了Presenter. 這里并沒有存Presenter的引用。
Presenter的構(gòu)造函數(shù)有兩個(gè)參數(shù), 一個(gè)是Model(Model類一般叫XXXRepository), 一個(gè)是View. 構(gòu)造中先用guava的checkNotNull()
檢查兩個(gè)參數(shù)是否為null, 然后賦值到字段; 之后再調(diào)用View的setPresenter()方法把Presenter傳回View中引用。
Model實(shí)現(xiàn)細(xì)節(jié)
Model只有一個(gè)類, 即TasksRepository. 它還是一個(gè)單例。 因?yàn)樵谶@個(gè)應(yīng)用的例子中, 我們操作的數(shù)據(jù)就這一份。
它由手動(dòng)實(shí)現(xiàn)的注入類Injection類提供:
public class Injection {
public static TasksRepository provideTasksRepository(@NonNull Context context) {
checkNotNull(context);
return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
TasksLocalDataSource.getInstance(context));
}
}
構(gòu)造如下:
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
@NonNull TasksDataSource tasksLocalDataSource) {
mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
}
數(shù)據(jù)分為local和remote兩大部分。 local部分負(fù)責(zé)數(shù)據(jù)庫的操作, remote部分負(fù)責(zé)網(wǎng)絡(luò)。 Model類中還有一個(gè)內(nèi)存緩存。
TasksDataSource是一個(gè)接口。 接口中定義了Presenter查詢數(shù)據(jù)的回調(diào)接口, 還有一些增刪改查的方法。
單元測(cè)試
MVP模式的主要優(yōu)勢(shì)就是便于為業(yè)務(wù)邏輯加上單元測(cè)試。
本例子中的單元測(cè)試是給TasksRepository和四個(gè)feature的Presenter加的。
Presenter的單元測(cè)試, Mock了View和Model, 測(cè)試調(diào)用邏輯, 如:
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
}
。。。
}
todo-mvp-loaders 用Loader取數(shù)據(jù)的MVP
基于上一個(gè)例子todo-mvp, 只不過這里改為用Loader來從Repository得到數(shù)據(jù)。
todo-mvp-loaders
使用Loader的優(yōu)勢(shì):
去掉了回調(diào), 自動(dòng)實(shí)現(xiàn)數(shù)據(jù)的異步加載;
當(dāng)內(nèi)容改變時(shí)回調(diào)出新數(shù)據(jù);
當(dāng)應(yīng)用因?yàn)閏onfiguration變化而重建loader時(shí), 自動(dòng)重連到上一個(gè)loader.
Diff with todo-mvp
既然是基于todo-mvp, 那么之前說過的那些就不再重復(fù), 我們來看一下都有什么改動(dòng):
git difftool -d todo-mvp
添加了兩個(gè)類:
TaskLoader和TasksLoader.
在Activity中new Loader類, 然后傳入Presenter的構(gòu)造方法。
Contract中View接口刪掉了isActive()方法, Presenter刪掉了populateTask()方法。
數(shù)據(jù)獲取
添加的兩個(gè)新類是TaskLoader和TasksLoader, 都繼承于AsyncTaskLoader, 只不過數(shù)據(jù)的類型一個(gè)是單數(shù), 一個(gè)是復(fù)數(shù)。
AsyncTaskLoader是基于ModernAsyncTask, 類似于AsyncTask,
把load數(shù)據(jù)的操作放在loadInBackground()里即可, deliverResult()方法會(huì)將結(jié)果返回到主線程, 我們?cè)趌istener的onLoadFinished()里面就可以接到返回的數(shù)據(jù)了, (在這個(gè)例子中是幾個(gè)Presenter實(shí)現(xiàn)了這個(gè)接口)。
TasksDataSource接口的這兩個(gè)方法:
List《Task》 getTasks();
Task getTask(@NonNull String taskId);
都變成了同步方法, 因?yàn)樗鼈兪窃趌oadInBackground()方法里被調(diào)用。
Presenter中保存了Loader和LoaderManager, 在start()方法里initLoader, 然后onCreateLoader返回構(gòu)造傳入的那個(gè)loader.
onLoadFinished()里面調(diào)用View的方法。 此時(shí)Presenter實(shí)現(xiàn)LoaderManager.LoaderCallbacks.
數(shù)據(jù)改變監(jiān)聽
TasksRepository類中定義了observer的接口, 保存了一個(gè)listener的list:
private List《TasksRepositoryObserver》 mObservers = new ArrayList《TasksRepositoryObserver》();
public interface TasksRepositoryObserver {
void onTasksChanged();
}
每次有數(shù)據(jù)改動(dòng)需要刷新UI時(shí)就調(diào)用:
private void notifyContentObserver() {
for (TasksRepositoryObserver observer : mObservers) {
observer.onTasksChanged();
}
}
在兩個(gè)Loader里注冊(cè)和注銷自己為TasksRepository的listener: 在onStartLoading()里add, onReset()里面remove方法。
這樣每次TasksRepository有數(shù)據(jù)變化, 作為listener的兩個(gè)Loader都會(huì)收到通知, 然后force load:
@Override
public void onTasksChanged() {
if (isStarted()) {
forceLoad();
}
}
這樣onLoadFinished()方法就會(huì)被調(diào)用。
todo-databinding
基于todo-mvp, 使用Data Binding library來顯示數(shù)據(jù), 把UI和動(dòng)作綁定起來。
說到ViewModel, 還有一種模式叫MVVM(Model-View-ViewModel)模式。
這個(gè)例子并沒有嚴(yán)格地遵循Model-View-ViewModel模式或者M(jìn)odel-View-Presenter模式, 因?yàn)樗扔昧薞iewModel又用了Presenter.
mvp-databinding
Data Binding Library讓UI元素和數(shù)據(jù)模型綁定:
layout文件用來綁定數(shù)據(jù)和UI元素;
事件和action handler綁定;
數(shù)據(jù)變?yōu)榭捎^察的, 需要的時(shí)候可以自動(dòng)更新。
Diff with todo-mvp
添加了幾個(gè)類:
StatisticsViewModel;
SwipeRefreshLayoutDataBinding;
TasksItemActionHandler;
TasksViewModel;
從幾個(gè)View的接口可以看出方法數(shù)減少了, 原來需要多個(gè)showXXX()方法, 現(xiàn)在只需要一兩個(gè)方法就可以了。
數(shù)據(jù)綁定
以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);
}
現(xiàn)在只需要這樣:
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);
}
因?yàn)樗袛?shù)據(jù)綁定的操作都寫在了xml里:
《TextView
android:id=“@+id/task_detail_description”
。。。
android:text=“@{task.description}” /》
事件綁定
數(shù)據(jù)綁定省去了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是這時(shí)候傳入的:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mViewDataBinding.setPresenter(mPresenter);
}
數(shù)據(jù)監(jiān)聽
在顯示List數(shù)據(jù)的界面TasksFragment, 僅需要知道數(shù)據(jù)是否為空, 所以它使用了TasksViewModel來給layout提供信息, 當(dāng)尺寸設(shè)定的時(shí)候, 只有一些相關(guān)的屬性被通知, 和這些屬性綁定的UI元素被更新。
public void setTaskListSize(int taskListSize) {
mTaskListSize = taskListSize;
notifyPropertyChanged(BR.noTaskIconRes);
notifyPropertyChanged(BR.noTasksLabel);
notifyPropertyChanged(BR.currentFilteringLabel);
notifyPropertyChanged(BR.notEmpty);
notifyPropertyChanged(BR.tasksAddViewVisible);
}
其他實(shí)現(xiàn)細(xì)節(jié)
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可能會(huì)被包在ActionHandler中, 比如TasksItemActionHandler.
ViewModel也可以作為View接口的實(shí)現(xiàn), 比如StatisticsViewModel.
SwipeRefreshLayoutDataBinding類定義的onRefresh()動(dòng)作綁定。
todo-mvp-clean
這個(gè)例子是基于Clean Architecture的原則:
The Clean Architecture.
關(guān)于Clean Architecture, 還可以看這個(gè)Sample App: Android-CleanArchitecture.
這個(gè)例子在todo-mvp的基礎(chǔ)上, 加了一層domain層, 把應(yīng)用分為了三層:
mvp-clean.png
Domain: 盛放了業(yè)務(wù)邏輯, domain層包含use cases或者interactors, 被應(yīng)用的presenters使用。 這些use cases代表了所有從presentation層可能進(jìn)行的行為。
關(guān)鍵概念
和基本的mvp sample最大的不同就是domain層和use cases. 從presenters中抽離出來的domain層有助于避免presenter中的代碼重復(fù)。
Use cases定義了app需要的操作, 這樣增加了代碼的可讀性, 因?yàn)轭惷从沉四康摹?/p>
Use cases對(duì)于操作的復(fù)用來說也很好。 比如CompleteTask在兩個(gè)Presenter中都用到了。
Use cases的執(zhí)行是在后臺(tái)線程, 使用command pattern. 這樣domain層對(duì)于Android SDK和其他第三方庫來說都是完全解耦的。
Diff with todo-mvp
每一個(gè)feature的包下都新增了domain層, 里面包含了子目錄model和usecase等。
UseCase是一個(gè)抽象類, 定義了domain層的基礎(chǔ)接口點(diǎn)。
UseCaseHandler用于執(zhí)行use cases, 是一個(gè)單例, 實(shí)現(xiàn)了command pattern.
UseCaseThreadPoolScheduler實(shí)現(xiàn)了UseCaseScheduler接口, 定義了use cases執(zhí)行的線程池, 在后臺(tái)線程異步執(zhí)行, 最后把結(jié)果返回給主線程。
UseCaseScheduler通過構(gòu)造傳給UseCaseHandler.
測(cè)試中用了UseCaseScheduler的另一個(gè)實(shí)現(xiàn)TestUseCaseScheduler, 所有的執(zhí)行變?yōu)橥降摹?/p>
Injection類中提供了多個(gè)Use cases的依賴注入, 還有UseCaseHandler用來執(zhí)行use cases.
Presenter的實(shí)現(xiàn)中, 多個(gè)use cases和UsseCaseHandler都由構(gòu)造傳入, 執(zhí)行動(dòng)作, 比如更新一個(gè)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
關(guān)鍵概念:
dagger2 是一個(gè)靜態(tài)的編譯期依賴注入框架。
這個(gè)例子中改用dagger2實(shí)現(xiàn)依賴注入。 這樣做的主要好處就是在測(cè)試的時(shí)候我們可以用替代的modules. 這在編譯期間通過flavors就可以完成, 或者在運(yùn)行期間使用一些調(diào)試面板來設(shè)置。
Diff with todo-mvp
Injection類被刪除了。
添加了5個(gè)Component, 四個(gè)feature各有一個(gè), 另外數(shù)據(jù)對(duì)應(yīng)一個(gè): TasksRepositoryComponent, 這個(gè)Component被保存在Application里。
數(shù)據(jù)的module: TasksRepositoryModule在mock和prod目錄下各有一個(gè)。
對(duì)于每一個(gè)feature的Presenter的注入是這樣實(shí)現(xiàn)的:
首先, 把Presenter的構(gòu)造函數(shù)標(biāo)記為@Inject, 然后在Activity中構(gòu)造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(
?。ǎ═oDoApplication) getApplication()).getTasksRepositoryComponent()).build()
.inject(this);
}
這個(gè)module里provide了view和taskId:
@Module
public class AddEditTaskPresenterModule {
private final AddEditTaskContract.View mView;
private String mTaskId;
public AddEditTaskPresenterModule(AddEditTaskContract.View view, @Nullable String taskId) {
mView = view;
mTaskId = taskId;
}
@Provides
AddEditTaskContract.View provideAddEditTaskContractView() {
return mView;
}
@Provides
@Nullable
String provideTaskId() {
return mTaskId;
}
}
注意原來構(gòu)造方法里調(diào)用的setPresenter方法改為用方法注入實(shí)現(xiàn):
/**
* Method injection is used here to safely reference {@code this} after the object is created.
* For more information, see Java Concurrency in Practice.
*/
@Inject
void setupListeners() {
mAddTaskView.setPresenter(this);
}
todo-mvp-contentproviders
這個(gè)例子是基于todo-mvp-loaders的, 用content provider來獲取repository中的數(shù)據(jù)。
mvp-contentproviders
使用Content Provider的優(yōu)勢(shì)是:
管理了結(jié)構(gòu)化數(shù)據(jù)的訪問;
Content Provider是跨進(jìn)程訪問數(shù)據(jù)的標(biāo)準(zhǔn)接口。
Diff with todo-mvp-loaders
注意這個(gè)例子是唯一一個(gè)不基于最基本的todo-mvp, 而是基于todo-mvp-loaders. (但是我覺得也可以認(rèn)為是直接從todo-mvp轉(zhuǎn)化的。)
看diff: git difftool -d todo-mvp-loaders.
去掉了TaskLoader和TasksLoader. (回歸到了基本的todo-mvp)。
TasksRepository中的方法不是同步方法, 而是異步加callback的形式。 (回歸到了基本的todo-mvp)。
TasksLocalDataSource中的讀方法都變成了空實(shí)現(xiàn), 因?yàn)镻resenter現(xiàn)在可以自動(dòng)收到數(shù)據(jù)更新。
新增LoaderProvider用來創(chuàng)建Cursor Loaders, 有兩個(gè)方法:
// 返回特定fiter下或全部的數(shù)據(jù)
public Loader《Cursor》 createFilteredTasksLoader(TaskFilter taskFilter)
// 返回特定id的數(shù)據(jù)
public Loader《Cursor》 createTaskLoader(String taskId)
其中第一個(gè)方法的參數(shù)TaskFilter, 用來指定過濾的selection條件, 也是新增類。
LoaderManager和LoaderProvider都是由構(gòu)造傳入Presenter, 在回調(diào)onTaskLoaded()和onTasksLoaded()中init loader.
在TasksPresenter中還做了判斷, 是init loader還是restart loader:
@Override
public void onTasksLoaded(List《Task》 tasks) {
// we don‘t care about the result since the CursorLoader will load the data for us
if (mLoaderManager.getLoader(TASKS_LOADER) == null) {
mLoaderManager.initLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this);
} else {
mLoaderManager.restartLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this);
}
}
其中initLoader()和restartLoader()時(shí)傳入的第二個(gè)參數(shù)是一個(gè)bundle, 用來指明過濾類型, 即是帶selection條件的數(shù)據(jù)庫查詢。
同樣是在onLoadFinshed()的時(shí)候做View處理, 以TaskDetailPresenter為例:
@Override
public void onLoadFinished(Loader《Cursor》 loader, Cursor data) {
if (data != null) {
if (data.moveToLast()) {
onDataLoaded(data);
} else {
onDataEmpty();
}
} else {
onDataNotAvailable();
}
}
數(shù)據(jù)類Task中新增了靜態(tài)方法從Cursor轉(zhuǎn)為Task, 這個(gè)方法在Presenter的onLoadFinished()和測(cè)試中都用到了。
public static Task from(Cursor cursor) {
String entryId = cursor.getString(cursor.getColumnIndexOrThrow(
TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID));
String title = cursor.getString(cursor.getColumnIndexOrThrow(
TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE));
String description = cursor.getString(cursor.getColumnIndexOrThrow(
TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION));
boolean completed = cursor.getInt(cursor.getColumnIndexOrThrow(
TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED)) == 1;
return new Task(title, description, entryId, completed);
}
另外一些細(xì)節(jié):
數(shù)據(jù)庫中的內(nèi)存cache被刪了。
Adapter改為繼承于CursorAdapter.
單元測(cè)試
新增了MockCursorProvider類, 用于在單元測(cè)試中提供數(shù)據(jù)。
其內(nèi)部類TaskMockCursor mock了Cursor數(shù)據(jù)。
Presenter的測(cè)試中仍然mock了所有構(gòu)造傳入的參數(shù), 然后準(zhǔn)備了mock數(shù)據(jù), 測(cè)試的邏輯主要還是拿到數(shù)據(jù)后的view操作, 比如:
@Test
public void loadAllTasksFromRepositoryAndLoadIntoView() {
// When the loader finishes with tasks and filter is set to all
when(mBundle.getSerializable(TaskFilter.KEY_TASK_FILTER)).thenReturn(TasksFilterType.ALL_TASKS);
TaskFilter taskFilter = new TaskFilter(mBundle);
mTasksPresenter.setFiltering(taskFilter);
mTasksPresenter.onLoadFinished(mock(Loader.class), mAllTasksCursor);
// Then progress indicator is hidden and all tasks are shown in UI
verify(mTasksView).setLoadingIndicator(false);
verify(mTasksView).showTasks(mShowTasksArgumentCaptor.capture());
}
todo-mvp-rxjava
關(guān)于這個(gè)例子, 之前看過作者的文章: Android Architecture Patterns Part 2:
Model-View-Presenter,
這個(gè)文章上過Android Weekly Issue #226.
這個(gè)例子也是基于todo-mvp, 使用RxJava處理了presenter和數(shù)據(jù)層之間的通信。
MVP基本接口改變
BasePresenter接口改為:
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
View在onResume()的時(shí)候調(diào)用Presenter的subscribe(); 在onPause()的時(shí)候調(diào)用presenter的unsubscribe()。
如果View接口的實(shí)現(xiàn)不是Fragment或Activity, 而是Android的自定義View, 那么在Android View的onAttachedToWindow()和onDetachedFromWindow()方法里分別調(diào)用這兩個(gè)方法。
Presenter中保存了:
private CompositeSubscription mSubscriptions;
在subscribe()的時(shí)候, mSubscriptions.add(subscription);;
在unsubscribe()的時(shí)候, mSubscriptions.clear(); 。
Diff with todo-mvp
數(shù)據(jù)層暴露了RxJava的Observable流作為獲取數(shù)據(jù)的方式, TasksDataSource接口中的方法變成了這樣:
Observable《List《Task》》 getTasks();
Observable《Task》 getTask(@NonNull String taskId);
callback接口被刪了, 因?yàn)椴恍枰恕?/p>
TasksLocalDataSource中的實(shí)現(xiàn)用了SqlBrite, 從數(shù)據(jù)庫中查詢出來的結(jié)果很容易地變成了流:
@Override
public Observable《List《Task》》 getTasks() {
。。。
return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)
.mapToList(mTaskMapperFunction);
}
TasksRepository中整合了local和remote的data, 最后把Observable返回給消費(fèi)者(Presenters和Unit Tests)。 這里用了.concat()和.first()操作符。
Presenter訂閱TasksRepository的Observable, 然后決定View的操作, 而且Presenter也負(fù)責(zé)線程的調(diào)度。
簡單的比如AddEditTaskPresenter中:
@Override
public void populateTask() {
if (mTaskId == null) {
throw new RuntimeException(“populateTask() was called but task is new.”);
}
Subscription subscription = mTasksRepository
.getTask(mTaskId)
.subscribeOn(mSchedulerProvider.computation())
.observeOn(mSchedulerProvider.ui())
.subscribe(new Observer《Task》() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
if (mAddTaskView.isActive()) {
mAddTaskView.showEmptyTaskError();
}
}
@Override
public void onNext(Task task) {
if (mAddTaskView.isActive()) {
mAddTaskView.setTitle(task.getTitle());
mAddTaskView.setDescription(task.getDescription());
}
}
});
mSubscriptions.add(subscription);
}
StatisticsPresenter負(fù)責(zé)統(tǒng)計(jì)數(shù)據(jù)的顯示, TasksPresenter負(fù)責(zé)過濾顯示所有數(shù)據(jù), 里面的RxJava操作符運(yùn)用比較多, 可以看到鏈?zhǔn)讲僮鞯奶攸c(diǎn)。
關(guān)于線程調(diào)度, 定義了BaseSchedulerProvider接口, 通過構(gòu)造函數(shù)傳給Presenter, 然后實(shí)現(xiàn)用SchedulerProvider, 測(cè)試用ImmediateSchedulerProvider. 這樣方便測(cè)試。
評(píng)論
查看更多