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文件夹下是我们主要的代码,展开的部分就是,可以看到是按照业务模块划分的,从上到下依次是添加模块,数据层,统计模块,详细模块,展示模块,工具类,PV基类,名字起的有些随意,再看每一个包中的具体类,以tasks为例:
- ScrollChildSwipeRefreshLayout—-自定义View
- TasksActivity——————–负责创建V,P
- TasksContract——————–接口,V,P接口的纽带
- TasksFilterType——————枚举类
- TasksFragment——————–View层实现类
- TasksPresenter——————-Presenter层实现类
先以代码的方式了解下View层和Presenter层是如果创建并工作的,先来看看Activity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tasks_act); TasksFragment tasksFragment = (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame); if (tasksFragment == null) { tasksFragment = TasksFragment.newInstance(); ActivityUtils.addFragmentToActivity( getSupportFragmentManager(), tasksFragment, R.id.contentFrame); } mTasksPresenter = new TasksPresenter( Injection.provideTasksRepository(getApplicationContext()), tasksFragment); if (savedInstanceState != null) { TasksFilterType currentFiltering = (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY); mTasksPresenter.setFiltering(currentFiltering); } }
|
可以看到在Activity初始化的时候分别创建了一个Fragment(View的实现类),一个TasksPresenter(Presenter的实现类),注意Presenter在构建的时候需要传入一个View对象。接下来看看Presenter初始化的时候都做了些什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <-- 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!"); mTasksView.setPresenter(this); }
|
先通过Injection的静态方法provideTasksRepository()创建一个TasksRepository(Model层的实现类),之后将其与Fragment通过构造函数传递到Presenter中,这样在P层初始化的时候就持有了M和V的对象。之后会通过View.setPresenter(P)方法为View层设置对应的Presenter。看一下Fragment中的代码:
1 2 3 4 5 6 7 8 9 10 11 12
| <-- 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层开始工作。
1 2 3 4 5 6
| <-- TasksFragment --> @Override public void onResume() { super.onResume(); mPresenter.start(); }
|
第二步: TasksPresenter.start()方法中调用了loadTasks()方法,我们需要在TasksContract.Presenter中去规定这个方法,然后再在TasksPresenter中去实现它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| <-- TasksPresenter --> @Override public void start() { loadTasks(false); } <-- TasksContract.Presenter --> void loadTasks(boolean forceUpdate); <-- TasksPresenter --> private boolean mFirstLoad = true; @Override public void loadTasks(boolean forceUpdate) { loadTasks(forceUpdate || mFirstLoad, true); mFirstLoad = false; } private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) { if (showLoadingUI) { mTasksView.setLoadingIndicator(true); } if (forceUpdate) { mTasksRepository.refreshTasks(); } EspressoIdlingResource.increment(); mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { List<Task> tasksToShow = new ArrayList<Task>(); if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) { EspressoIdlingResource.decrement(); } 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; } } if (!mTasksView.isActive()) { return; } if (showLoadingUI) { mTasksView.setLoadingIndicator(false); } processTasks(tasksToShow); } @Override public void onDataNotAvailable() { if (!mTasksView.isActive()) { return; } mTasksView.showLoadingTasksError(); } }); }
|
第三步:调用TasksRepository.getTasks()方法,所以需要在TasksDataSource中添加getTasks,然后让TasksRepository去实现这个方法,在这个方法中调用具体的数据层的实现类mTasksRemoteDataSource,mTasksLocalDataSource中的getTasks,之后通过传递过来的接口将数据返回到Presenter中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <-- TasksRepository --> private final TasksDataSource mTasksRemoteDataSource; private final TasksDataSource mTasksLocalDataSource; Map<String, Task> mCachedTasks; @Override public void getTasks(@NonNull final LoadTasksCallback callback) { checkNotNull(callback); if (mCachedTasks != null && !mCacheIsDirty) { callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); return; } if (mCacheIsDirty) { getTasksFromRemoteDataSource(callback); } else { 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 tasks);然后在TasksFragment中去实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <-- TasksPresneter --> private void processTasks(List<Task> tasks) { if (tasks.isEmpty()) { processEmptyTasks(); } else { mTasksView.showTasks(tasks); 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类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <--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() { MockitoAnnotations.initMocks(this); mTasksPresenter = new TasksPresenter(mTasksRepository, mTasksView); when(mTasksView.isActive()).thenReturn(true); TASKS = Lists.newArrayList(new Task("Title1", "Description1"), new Task("Title2", "Description2", true), new Task("Title3", "Description3", true)); }
|
如果上面的代码,看不明白,那还是去阅读我刚才推荐的文章,这里就简单的说一下,先是mock了两个对象,相关的model和view
通过@Before``` 注解,在所有的测试方法做初始化,依据mock的对象,创建一个presenter,设置测试桩,初始化数据。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 接下来只看一个方法: ```java <--TasksPresenterTest> @Test public void loadAllTasksFromRepositoryAndLoadIntoView() { mTasksPresenter.setFiltering(TasksFilterType.ALL_TASKS); mTasksPresenter.loadTasks(true); verify(mTasksRepository).getTasks(mLoadTasksCallbackCaptor.capture()); mLoadTasksCallbackCaptor.getValue().onTasksLoaded(TASKS); InOrder inOrder = inOrder(mTasksView); inOrder.verify(mTasksView).setLoadingIndicator(true); inOrder.verify(mTasksView).setLoadingIndicator(false); ArgumentCaptor<List> showTasksArgumentCaptor = ArgumentCaptor.forClass(List.class); verify(mTasksView).showTasks(showTasksArgumentCaptor.capture()); assertTrue(showTasksArgumentCaptor.getValue().size() == 3); }
|
代码中对每一句都进行了注释,这里对参数捕获器说一下,最开始我不明白这个东西是个什么玩意,怎么工作的,网上查就说是参数捕获器,能够捕捉到一个方法的入参的相关信息。然后自己就照着demo写,写完发现一运行报错了,如果是验证方法, 或者断音之类的问题,会有相关提示的,我这报错没有啊。
我还以为是为代码写的有问题,就把google里的代码拷过来,运行,还是不行,这就奇怪了,为什么同样的的代码在别人那就没问题,在我这就报错那,之后就开始排查问题,先一句句从下往上注释掉,运行,看看是哪句出的问题,被我发现了是这句java verify(mTasksView).showTasks(showTasksArgumentCaptor.capture());
,这我就更不明白了,这个是展示数据的,肯定没问题的啊,然后就去原代码中去排查,从上到下看了一遍,没问题,又看了一遍没问题,然后又回到测试代码,各种改,想看看是哪的问题,就上面的那几句测试代码,我玩了半天,还是没有找到为什么,不行了,估计是自己对mock这个东西不是很了解,就去查网上的资料,看各种译文,实例文章,介绍文章。当看到这的时候我好想似乎明白了些什么。
是不是view.showTasks()方法没有执行啊,那样的话参数就不会被捕获,所以就去之前正式代码中起添加打印语句,发现,确实没有执行,为什么没有指定那,继续往上找在数据遍历的时候:
我擦嘞,我居然把集合写错了,可能是敲的时候没注意直接就确定了,也没看是哪一个了,改过来之后再运行,总终于成功了,就这么个问题,搞了我一天半,不过经历了这么个事情之后,我发现对于这些基本的测试桩,验证,断言,顺序执行,熟悉的不要不要的,真是没有磨难就没有进步啊。
这样一个简单的单元测试就完成了,测试内容那就是presenter加载数据,验证model获取数据,设置接口回调参数,验证view方法执行顺序,验证view方法是否执行,捕获参数,比对参数内数据。
多说几句
说一下我在学习这个项目的一些心得体会吧:
- 代码不是看的,一定要敲,不知道大家怎么去学习别人的项目,在我看来,最好的学习方式,就是把别人的项目敲一遍,看的时候有可能不过脑,敲的时候就肯定得思考了,为什么这么分包,应该怎么调用,之类的。
- 什么东西都不要浮于表面,因为现在已经不是那个,我见过,我了解的时代了。要尽可能的做到,我知道,我熟悉,我敲过,我写过相关案例, 我看过源码,我了解底层实现。
- 关于这个项目还有一部分没有介绍,那就是UI测试,目前正在看资料,之后应该会出一篇,不过应该是在整个项目都敲完的时候了。其实个人感觉,单元测试这个东西,重在经验,如果其实很容易,五六个注解,三四个方法,打桩,验证,断言,任何一个人估计半天到一天应该都差不多,感觉更重要的是正式代码的书写,如果正式代码写的不好,测试代码写都不能写,跟别说验证了,而且测试经验很重要,只有经历足够多的测试案例,才会真正掌握单元测试的精髓吧,所谓的测试驱动开发,想想就觉得好激动。