Google Samples 学习之 todo-mvp(二)

之前的文章对 todo-mvp 项目的 Gradle 依赖和运行效果进行了分析,本文则要对项目的代码组织结构进行简要分析。 前置文章:Google Samples 学习之 todo-mvp(一)

文件结构

主要代码文件的组织情况如下图所示:

dir.PNG

src 目录是编程工作的主要工作目录,按图中从上到下的顺序介绍它的子目录的内容有:

  • androidTest:UI 测试类
  • androidTestMock:UI 测试相关的 mock 测试类
  • main:主要代码
  • mock:对应 flavor 为 mock 的情况时,手动注入 Repository 实例(Model 层访问工具)
  • prod:对应 flavor 为 prod 的情况时,手动注入 Repository 实例
  • test:单元测试类

对 main 目录中的结构要特别的关注,在包目录下又细分了几个子目录,按照功能划分来管理代码:

  • addedittask:增加新任务
  • data:数据操作相关类,即 Model 层主要代码
  • statistics:任务统计
  • taskdetail:任务详情
  • tasks:任务总览
  • util:工具类

src 目录下还有两个单独的文件 BaseView 和 BasePresenter,分别是 View 和 Presenter 层必须实现的接口,定义了各自层中的成员的行为。

BaseView 的代码如下:

1
2
3
public interface BaseView<T> {
    void setPresenter(T presenter);
}
  • 通过泛型保证 View 和 Presenter 的对应
  • 接口起到了注入 Presenter 实例的作用
  • 而且说明 View 对象要保持 Presenter 对象的引用

BasePresenter 的代码如下:

1
2
3
public interface BasePresenter {
    void start();
}

实际上接口没有对 Presenter 行为做出特别细致的定义,具体的 Presenter 对象的工作流程要靠程序员自己编写。

代码分析

可以注意到界面相关的功能块都会涉及到 View 和 Presenter 的操作,它们的连接则是通过各种 Contract 接口定义。 比如,添加任务的相关接口为 AddEditTaskContract:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public interface AddEditTaskContract {

    interface View extends BaseView<Presenter> {
        void showEmptyTaskError();
        void showTasksList();
        void setTitle(String title);
        void setDescription(String description);
        boolean isActive();
    }

    interface Presenter extends BasePresenter {
        void saveTask(String title, String description);
        void populateTask();
        boolean isDataMissing();
    }
}

可以看到 Contract 接口内部包含两个接口 View 和 Presenter,继承 BaseView 和 BasePresenter,分别定义这个功能所需的 View 和 Presenter 的行为。 使用 Contract 接口的方法让代码结构非常清晰,而且维护非常方便。而且实现同一功能的代码都在一个目录,即使接口发生更改,后续的修改也不会它麻烦。

因为项目较为简单,util 目录下只有 3 个工具类

util.PNG

内部只有一个静态方法用来向 Activity 添加 Fragment 实例(毕竟这段代码重复度极高):

1
2
3
4
5
6
7
8
public static void addFragmentToActivity (@NonNull FragmentManager fragmentManager,
                                              @NonNull Fragment fragment, int frameId) {
    checkNotNull(fragmentManager);
    checkNotNull(fragment);
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    transaction.add(frameId, fragment);
    transaction.commit();
}

特别要注意的是 checkNotNull() 方法在 Guava 库中定义,用来检查对象的非空性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
* Ensures that an object reference passed as a parameter to the calling method is not null.
*
* @param reference an object reference
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
public static <T> T checkNotNull(T reference) {
    if (reference == null) {
        throw new NullPointerException();
    }
    return reference;
}

注意:这么做就是为了一个简单的方法引入了庞大的 Guava 库,自己写代码的时候就没必要同样使用 Guava 了,不如另外写一个工具类包含 checkNotNull() 方法。

Espresso 的测试程序和被测应用是同步执行的。Espresso 等待待测试资源处于空闲状态,才会执行下个动作和检查下个断言。 默认情况下,Espresso 会检查 UI 线程任务以及 AsyncTask 线程池,确保当前没有任务使用资源,即空闲状态。但是,还有一些后台任务的线程状态不像以上两种那么容易检测(比如使用 Intentservice 进行按钮点击操作),默认检查流程会导致 Espresso 在还有后台任务的情况下误判资源处于空闲状态 —— 这时就需要程序员编写代码通知 Espresso 任务状态,以明确正确的测试开始时刻。 好在,Espresso 提供了接口 IdlingResource,实现它就可以轻松通知 Espresso 资源是否空闲。

更多指导内容可参考 IdlingResource 官方文档

这个简单的实现使用了一个内部计数器来实现空闲状态检查:

  • 如果资源被一个线程使用则计数器加一
  • 如果资源被一个线程释放、线程工作结束或中止,则计数减一
  • 如果计数为零,则通知 Espresso 资源空闲
  • 如果计数为负数,则说明计数器被破坏或者计数过程错误,要抛出异常

实际上最重要的代码是:

 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
public final class SimpleCountingIdlingResource implements IdlingResource {

    private final String mResourceName;

    private final AtomicInteger counter = new AtomicInteger(0);

    // written from main thread, read from any thread.
    private volatile ResourceCallback resourceCallback;

    ... // 省略其他不重要的代码

    /**
     * Increments the count of in-flight transactions to the resource being monitored.
     */
    public void increment() {
        counter.getAndIncrement();
    }

    /**
     * Decrements the count of in-flight transactions to the resource being monitored.
     *
     * If this operation results in the counter falling below 0 - an exception is raised.
     *
     * @throws IllegalStateException if the counter is below 0.
     */
    public void decrement() {
        int counterVal = counter.decrementAndGet();
        if (counterVal == 0) {
            // we've gone from non-zero to zero. That means we're idle now! Tell espresso.
            if (null != resourceCallback) {
                resourceCallback.onTransitionToIdle();
            }
        }

        if (counterVal < 0) {
            throw new IllegalArgumentException("Counter has been corrupted!");
        }
    }

注意:要使用 AtomicInteger 以确保自增和自减操作的原子性,原有自增和自减操作符 ++、– 的操作不是原子的。

其主要代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * Contains a static reference to {@link IdlingResource}, only available in the 'mock' build type.
 */
public class EspressoIdlingResource {

    private static final String RESOURCE = "GLOBAL";

    private static SimpleCountingIdlingResource mCountingIdlingResource =
            new SimpleCountingIdlingResource(RESOURCE);

    public static void increment() {
        mCountingIdlingResource.increment();
    }

    public static void decrement() {
        mCountingIdlingResource.decrement();
    }

    public static IdlingResource getIdlingResource() {
        return mCountingIdlingResource;
    }
}

可以看出 EspressoIdlingResource 使用了 SimpleCountingIdlingResource 的功能实现了一个针对全局资源的空闲状态检查类。这是因为编写 app 的程序员知道:这个 app 中使用后台任务访问资源的行为都集中在 Model 层的数据获取行为,而 Model 层获取数据时使用的资源辅助类都是全局可用的(虽然其他层根本不会使用它)。 具体的使用方法也很简单:

  • 在获取数据之前调用静态方法 increment
  • 在获取数据成功后调用方法 decrement

后续文章

后续会对目录 addedittask、data 中的代码进行分析。statistics 和 taskdetail 目录同 addedittask 目录结构没有太大区别,代码的组织和实现方式也同 addedittask、tasks 相似,因此不重复分析。

参考文章

Espresso:自定义Idling Resource