Model与远程和本地数据源协作,以获取和保存数据。这里正是处理业务逻辑的地方。例如,当请求任务列表时,Model会试着先在本地数据源查询。如果没用,它就请求网络,并将响应的数据保存在本地数据源中,最后返回列表。
任务查询是在RxJava的帮助下来完成的:
public Observable> getTasks(){
...
}
Model通过构造方法持有本地和远程数据源,这使得Model完全独立于Android类,从而便于做单元测试。例如,为了测试getTasks从本地请求数据,我们实现以下测试方法:
@Mock
private TasksDataSource mTasksRemoteDataSource;
@Mock
private TasksDataSource mTasksLocalDataSource;
...
@Test
public void getTasks_requestsAllTasksFromLocalDataSource() {
// Given that the local data source has data available
setTasksAvailable(mTasksLocalDataSource, TASKS);
// And the remote data source does not have any data available
setTasksNotAvailable(mTasksRemoteDataSource);
// When tasks are requested from the tasks repository
TestSubscriber> testSubscriber = new TestSubscriber<>();
mTasksRepository.getTasks().subscribe(testSubscriber);
// Then tasks are loaded from the local data source
verify(mTasksLocalDataSource).getTasks();
testSubscriber.assertValue(TASKS);
}
View
View与Presenter一起工作,显示数据,并通知Presenter关于用户的行为。在MVP中,Activity、Fragment与自定义控件都可作为View。这里我们使用Fragment。
所有View都实现了一个可以关联Presenter的BaseView接口。
public interface BaseView {
void setPresenter(T presenter);
}
View会在onResume中调用Presenter的subscribe方法来告知Presenter它已经准备好做更新操作。而在onPause中调用presenter.unsubscribe()来告知Presenter它不再接收更新操作。如果View的实现是一个自定义控件,那么subscribe与unsubscribe方法会在onAttachedToWindow与onDetachedFromWindow中调用。用户行为,如点击按钮,将触发Presenter中相应的方法,这决定了下一步流程。
我们使用Espresso来测试View。例如,要在屏幕上显示活动任务和已完成任务列表。单元测试首先会创建一些任务放在TaskRepository中,然后启动StatisticsActivity来检测是否正确显示:
@Before
public void setup() {
// Given some tasks
TasksRepository.destroyInstance();
TasksRepository repository = Injection.provideTasksRepository(
InstrumentationRegistry.getContext());
repository.saveTask(new Task("Title1", "", false));
repository.saveTask(new Task("Title2", "", true));
// Lazily start the Activity from the ActivityTestRule
Intent startIntent = new Intent();
mStatisticsActivityTestRule.launchActivity(startIntent);
}
@Test
public void Tasks_ShowsNonEmptyMessage() throws Exception {
// Check that the active and completed tasks text is displayed
Context context = InstrumentationRegistry.getTargetContext();
String expectedActiveTaskText = context
.getString(R.string.statistics_active_tasks);
onView(withText(containsString(expectedActiveTaskText)))
.check(matches(isDisplayed()));
String expectedCompletedTaskText = context
.getString(R.string.statistics_completed_tasks);
onView(withText(containsString(expectedCompletedTaskText)))
.check(matches(isDisplayed()));
}
Presenter
Presenter与其相应的View由Activity创建;View与TaskRepository(即Model)的引用由构造方法传入;在构造方法中,Presenter将调用View的setPresenter方法。以上过程可以由依赖注入框架简化,允许向Presenter注入相应的View,以降低类之间的耦合度。这个方案可以在另一个由Dagger实现的实现中找到。
所有Presenter都实现了BasePresetner接口。
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
当subscribe方法调用时,Presenter向Model请求数据,然后将UI逻辑应用于接收到的数据,最后传递给View。例如,在StatisticsPresenter中,所有任务都是向TaskRepository查询得到的,查询结果会用于计算活动任务和已完成任务的数量。这些数量将用作View方法showStatistics(int numberOfActiveTasks, int numberOfCompletedTasks)的参数。
一个检测showStatistics是否调用了正确值的单元测试是很好实现的。我们将模拟TaskRepository和StatisticsContract.View,然后将模拟对象作为StatisticsPresenter构造方法的参数。以下是测试实现:
@Test
public void loadNonEmptyTasksFromRepository_CallViewToDisplay() {
// Given an initialized StatisticsPresenter with
// 1 active and 2 completed tasks
setTasksAvailable(TASKS);
// When loading of Tasks is requested
mStatisticsPresenter.subscribe();
// Then the correct data is passed on to the view
verify(mStatisticsView).showStatistics(1, 2);
}
unsubscribe的作用是清除Presenter中的观察者,防止内存泄露。
除了subscribe与unsubscribe,每个Presenter也会依据View中的用户行为暴露一些其它方法。例如,AddEditTaskPresenter会添加如createTask这样的方法,来响应用户点击任务创建按钮的行为。这保证所有用户行为(以及随后的UI逻辑)都通过Presenter,从而可以进行单元测试。