GOOGLE TODO-MVP 学习笔记
背景(可忽略):《GOOGLE TODO-MVP 学习笔记》这篇文章主要会记录自己在根据TODO-MVP这个项目学习MVP的过程中的一些心得和想法,一是为了自己记录下来,二是为了说出来,增强自己的理解。
由于时间及经验有限,文中可能存在错误与不足,欢迎大家指出,我会第一时间对文章进行修改纠正。
如果对MVP模式不是很了解的,可以先去看看相关文章,这里推荐diygreen的两篇文章,MVP详解上下。
google 项目地址:https://github.com/googlesamples/android-architecture/
选择不同的分支,本文的是TODO-MVP,也是最基础的。
本文主要讲了两个部分
- 在TODO-MVP中是如何实现MVP的
- 一个简单的单元测试
TODO-MVP
先来一个整体的概览:
整个项目结构特别清晰,最外层是五个文件夹,两个代码目录,三个测试目录,之前看文章有说四个测试目录的,不过个人不是很认同。其中在main文件夹下是我们主要的代码(找不到的请切换到Project结构),展开的部分就是,可以看到是按照业务模块划分的,从上到下依次是添加模块,数据层,统计模块,详细模块,展示模块,工具类,PV基类,名字起的有些随意,再看每一个包中的具体类,以tasks为例:
- ScrollChildSwipeRefreshLayout----自定义View
- TasksActivity-----------------------------负责创建V,P
- TasksContract---------------------------接口,V,P接口的纽带
- TasksFilterType-------------------------枚举类
- TasksFragment-------------------------View层实现类
- TasksPresenter-------------------------Presenter层实现类
先以代码的方式了解下View层和Presenter层是如果创建并工作的,先来看看Activity:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tasks_act);
//UI相关初始化,忽略
//通过工具类创建一个Fragment,是View层的实现类
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
// 创建一个Presenter
mTasksPresenter = new TasksPresenter(
Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
// Load previously saved state, if available.
//恢复界面中Task的类别
if (savedInstanceState != null) {
TasksFilterType currentFiltering =
(TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
}
}
可以看到在Activity初始化的时候分别创建了一个Fragment(View的实现类),一个TasksPresenter(Presenter的实现类),注意Presenter在构建的时候需要传入一个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层设置对应的Presenter层对象
mTasksView.setPresenter(this);
}
先通过Injection的静态方法provideTasksRepository()创建一个TasksRepository(Model层的实现类),之后将其与Fragment通过构造函数传递到Presenter中,这样在P层初始化的时候就持有了M和V的对象。之后会通过View.setPresenter(P)方法为View层设置对应的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的基类中声明抽象的设置方法,然后在Presenter初始化的时候将Presenter注入到View中。
总结一下:
- 在Activity创建的时候创建一个View对象,一个Presenter对象。
- 在创建presenter的时候将一个Model,上一步中创建好的View,通过构造函数注入到Presenter中。
- 在Presenter的构造方法中,通过View.setPresenter(P)方法,将Presenter设置到View中。
接下来用一个简单的例子来走一遍整体流程,以添加一个loadTasks为例:
第一步: 在TasksFragment的onResume()方法中,Presenter层开始工作。
<-- TasksFragment -->
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
第二步: TasksPresenter.start()方法中调用了loadTasks()方法,我们需要在TasksContract.Presenter中去规定这个方法,然后再在TasksPresenter中去实现它。
<-- 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
//调用TasksRepository.getTasks方法去获取数据
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();
}
});
}
第三步:调用TasksRepository.getTasks()方法,所以需要在TasksDataSource中添加getTasks,然后让TasksRepository去实现这个方法,在这个方法中调用具体的数据层的实现类mTasksRemoteDataSource,mTasksLocalDataSource中的getTasks,之后通过传递过来的接口将数据返回到Presenter中。
<-- TasksRepository -->
//具体的远程数据实现类
private final TasksDataSource mTasksRemoteDataSource;
//具体的本地数据实现类
private final TasksDataSource mTasksLocalDataSource;
//内存缓存
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);
}
});
}
}
第四步:调用mTasksView.showTasks()展示数据,所以需要在TasksContract.View中定义方法 void showTasks(List<Task> tasks);然后在TasksFragment中去实现。
<-- 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
//显示查询回来的数据
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中调用Presenter方法去请求数据,Presenter中调用Model方法去获取数据,Model在调用具体实现方法,获取数据之后,将数据通过接口返回到Presenter中,之后再调用View的方法展示数据。
但是,如果是按照上面的顺序去写代码的话,肯定会觉得这实在是太复杂了,多写好多东西,所以个人猜测应该不是按照上面的方式去写的,猜测应该是这样:
第一步:在TasksContract.Presenter中去写一个方法让它去加载数据,比如LoadTasks();
第二步:在TasksDataSource中写一个方法让它去获取数据,比如getTasks(),之后再定义一个接口,用于传递数据,抽象一个方法参数是Task集合,一个简单的接口回调。
第三步:在TasksContract.View中写一个方法,用于展示数据,参数肯定是要展示的数据了,比如showTasks(tasks);
第四步:写各自的实现方法。在View中不用考虑数据是怎么来的,只管UI的变化就好;在Presenter中不用管怎么展示,怎么获取数据,只管应该找谁要数据,之后处理一下,交给View去显示就好了;在Model中,只需要得到数据,传递给Presenter就可以了,其他的完全不用操心。
在每一个自己的层级中做自己应该做的事情,并且对其他的东西尽量少的了解,尽可能的不出现干涉,专注做自己的事情,这样代码写起来其实会清晰很多,更加富有条理,而且在以后的扩展或者修改会变得更加的容易,而不会有那种牵一发而动全身的感觉。
不知道各位对上面第二种写代码的方法觉得怎么样,个人认为,当接口方法确定了之后,其实整个开发工作基本上就完成百分之七十了,在View中不用去考虑业务逻辑,不用去考虑UI的变化,因为数据传递过来之后所有的事情就都已经确定了,在Presenter中不用去考虑数据的来源,在Model中不去考虑数据的预处理和变换,将所有需要做的功能或者是动作都尽可能的细化,细化到每一层的每一个方法中,在一个方法中只做一件事情,其他的并不知道,也不需要知道,剩下的工作就是简单的填充代码了。
一个简单的单元测试
其实在TODO-MVP中测试的代码要比正式的代码要多,虽然没有具体数过,不过从目录数量来看就已经证明了一点,测试真的很重要,我之前从来没有写过任何测试代码,也没有专门学过,只是在平时看了几篇测试相关的文章,太深的讲不了,太浅的说着也没意思,就拿一个例子来说,当然,在说具体的例子之前,如果各位对相关的单元测试的知识不是很了解的话,推荐大家几篇文章 。
邹小创,相关测试文章十一篇,由浅入深,通俗易懂。
键盘男,介绍一些实际测试中的经验。
单元测试利器-Mockito 中文文档介绍Mockito相关API和使用方法,很全面。
看过以上大神的文章之后,自己再随便浏览一些相关文章,基本上就没问题了。
接下来就是例子了,代码是在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了两个对象,相关的model和view。通过@Before
注解,在所有的测试方法做初始化,依据mock的对象,创建一个presenter,设置测试桩,初始化数据。
接下来只看一个方法:
<--TasksPresenterTest>
@Test
public void loadAllTasksFromRepositoryAndLoadIntoView() {
// Given an initialized TasksPresenter with initialized tasks
// When loading of Tasks is requested
//提供一个筛选的Task类型
mTasksPresenter.setFiltering(TasksFilterType.ALL_TASKS);
//加载数据
mTasksPresenter.loadTasks(true);
// Callback is captured and invoked with stubbed tasks
//验证model的加载数据的方法是否执行,并且对入参进行捕获
verify(mTasksRepository).getTasks(mLoadTasksCallbackCaptor.capture());
//设置接口回调传递回来的数据
mLoadTasksCallbackCaptor.getValue().onTasksLoaded(TASKS);
// Then progress indicator is shown
//验证mock view的方法执行顺序,创建一个inorder对象
InOrder inOrder = inOrder(mTasksView);
//验证view.setLoadingIndicator(true)是否执行
inOrder.verify(mTasksView).setLoadingIndicator(true);
// Then progress indicator is hidden and all tasks are shown in UI
//验证view.setLoadingIndicator(false)是否执行
inOrder.verify(mTasksView).setLoadingIndicator(false);
//创建一个参数捕获器
ArgumentCaptor<List> showTasksArgumentCaptor = ArgumentCaptor.forClass(List.class);
//验证view.showTasks()方法是否执行,并且执行对其参数进行捕获
verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
//断言 判断捕获的参数也就是传入showTasks方法中的list的size是否为3
assertTrue(showTasksArgumentCaptor.getValue().size() == 3);
}
代码中对每一句都进行了注释,很好理解,这里对参数捕获器说一下,最开始我不明白这个东西是个什么玩意,怎么工作的,网上查就说是参数捕获器,能够捕捉到一个方法的入参的相关信息。然后自己就照着demo写,写完发现一运行报错了,如果是验证方法, 或者断音之类的问题,会有相关提示的,我这报错没有啊。
我还以为是为代码写的有问题,就把google里的代码拷过来,运行,还是不行,这就奇怪了,为什么同样的的代码在别人那就没问题,在我这就报错那,之后就开始排查问题,先一句句从下往上注释掉,运行,看看是哪句出的问题,被我发现了是这句verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
,这我就更不明白了,这个是展示数据的,肯定没问题的啊,然后就去原代码中去排查,从上到下看了一遍,没问题,又看了一遍没问题,然后又回到测试代码,各种改,想看看是哪的问题,就上面的那几句测试代码,我玩了半天,还是没有找到为什么,不行了,估计是自己对mock这个东西不是很了解,就去查网上的资料,看各种译文,实例文章,介绍文章。当看到下面内容的时候我好想似乎明白了些什么。
是不是view.showTasks()方法没有执行啊,那样的话参数就不会被捕获,所以就去之前正式代码中添加打印语句,发现,确实没有执行,为什么没有执行那,继续往上找,在数据遍历的时候:
我擦嘞,我居然把集合写错了,可能是敲的时候没注意直接就确定了,也没看是哪一个了,改过来之后再运行,总终于成功了,就这么个问题,搞了我一天半,不过经历了这么个事情之后,我发现对于这些基本的测试桩,验证,断言,顺序执行,熟悉的不要不要的,真是没有磨难就没有进步啊。
这样一个简单的单元测试就完成了,测试内容那就是presenter加载数据,验证model获取数据,设置接口回调参数,验证view方法执行顺序,验证view方法是否执行,捕获参数,比对参数内数据。
多说几句
说一下我在学习这个项目的一些心得体会吧:
- 代码不是看的,一定要敲。不知道大家怎么去学习别人的项目,在我看来,最好的学习方式,就是把别人的项目敲一遍,看的时候有可能不过脑,敲的时候就肯定得思考了,为什么这么分包,应该怎么调用,之类的。
- 什么东西都不要浮于表面。因为现在已经不是那个,我见过,我了解的时代了。要尽可能的做到,我知道,我熟悉,我敲过,我写过相关案例, 我看过源码,我了解底层实现。
- 关于这个项目还有一部分没有介绍,那就是UI测试,目前正在看资料,之后应该会出一篇,不过应该是在整个项目都敲完的时候了。其实个人感觉,单元测试这个东西,重在经验,入门其实很容易,五六个注解,三四个方法,打桩,验证,断言,任何一个人估计半天到一天应该都差不多,感觉更重要的是正式代码的书写,如果正式代码写的不好,测试代码写都不能写,更别说验证了,而且测试经验很重要,只有经历足够多的测试案例,才会真正掌握单元测试的精髓吧,所谓的测试驱动开发,想想就觉得好激动。
- 我这才刚入门,还差得远那。