GOOGLE TODO-MVP 学习笔记

作者: one_cup | 来源:发表于2016-12-03 17:55 被阅读274次

    GOOGLE TODO-MVP 学习笔记

    背景(可忽略):《GOOGLE TODO-MVP 学习笔记》这篇文章主要会记录自己在根据TODO-MVP这个项目学习MVP的过程中的一些心得和想法,一是为了自己记录下来,二是为了说出来,增强自己的理解。
    由于时间及经验有限,文中可能存在错误与不足,欢迎大家指出,我会第一时间对文章进行修改纠正。

    如果对MVP模式不是很了解的,可以先去看看相关文章,这里推荐diygreen的两篇文章,MVP详解上下。
    google 项目地址:https://github.com/googlesamples/android-architecture/
    选择不同的分支,本文的是TODO-MVP,也是最基础的。

    本文主要讲了两个部分

    1. 在TODO-MVP中是如何实现MVP的
    2. 一个简单的单元测试

    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中。

    总结一下:

    1. 在Activity创建的时候创建一个View对象,一个Presenter对象。
    2. 在创建presenter的时候将一个Model,上一步中创建好的View,通过构造函数注入到Presenter中。
    3. 在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方法是否执行,捕获参数,比对参数内数据。

    多说几句

    说一下我在学习这个项目的一些心得体会吧:

    1. 代码不是看的,一定要敲。不知道大家怎么去学习别人的项目,在我看来,最好的学习方式,就是把别人的项目敲一遍,看的时候有可能不过脑,敲的时候就肯定得思考了,为什么这么分包,应该怎么调用,之类的。
    2. 什么东西都不要浮于表面。因为现在已经不是那个,我见过,我了解的时代了。要尽可能的做到,我知道,我熟悉,我敲过,我写过相关案例, 我看过源码,我了解底层实现。
    3. 关于这个项目还有一部分没有介绍,那就是UI测试,目前正在看资料,之后应该会出一篇,不过应该是在整个项目都敲完的时候了。其实个人感觉,单元测试这个东西,重在经验,入门其实很容易,五六个注解,三四个方法,打桩,验证,断言,任何一个人估计半天到一天应该都差不多,感觉更重要的是正式代码的书写,如果正式代码写的不好,测试代码写都不能写,更别说验证了,而且测试经验很重要,只有经历足够多的测试案例,才会真正掌握单元测试的精髓吧,所谓的测试驱动开发,想想就觉得好激动。
    4. 我这才刚入门,还差得远那。

    相关文章

      网友评论

        本文标题:GOOGLE TODO-MVP 学习笔记

        本文链接:https://www.haomeiwen.com/subject/qgycmttx.html