前言

Android Architecture Blueprints 是谷歌官方开源的一组 Android 架构方案,项目以多个分支分别采用 MVPMVVM 的概念以及 dagger、rxjava、databinding、livedata 等工具库,演示了一个简单的 TodoApp 在不同架构模式下的代码组织方式。这个项目中的代码非常规范,架构模式也非常有借鉴意义,是一个很有价值的学习素材。

本文将通过 项目组织架构组织与通信设计原则 这三个层次对项目中的todo-mvp分支进行解析,从而达到学习的目的。

项目概览

在分析之前,我们先通过截图来了解一下TodoApp的功能。

TodoApp
用来跳转列表页与统计页的抽屉窗口统计页
列表页
详情页编辑页

知晓了项目功能后,再来看项目的组织。

项目组织

在开始之前推荐大家先了解一下IDEA符号表,熟悉符号表对理解项目组织有很大帮助。

总览

TodoMVP 项目目录 app/src 除了 androidTest、main、test 这三个常见的模板目录外,还有androidTestMock、mock与prod 目录。androidTestMock 虽不常见,但通过名称可推断它是一个测试相关的目录,另外还有 mock、prod 这两个目录,我们稍后会讲到。 ## UI模块划分 TodoMVP 项目目录 app/src/main/java 下的 addedittask、statistics、taskdetail、tasks 这四个目录分别对应 App 内编辑页、统计页、详情页、列表页这四个模块,每个目录都有自己的 Activity、Fragment与Presenter,分别表示 MVP 中的 **View** 层和 **Presenter** 层,Contract 由字面意义推断为契约接口,此处暂且不表,稍后会在源码中了解其实际内涵。 ## Model层 TodoMVP 项目目录 app/src/main/java 下的 data 目录表示 **Model** 层,Task 是经final修饰的实体类模型,data/source 下有表示数据源接口的 TasksDataSource 与表示数据仓库类的 TasksRepository,data/source/local 与 data/source/remote 分别表示本地数据源与远程数据源,由ToDoDatabase及TastsDao可推断其local部分以数据库的形式实现。

最下方的 BasePresenter、BaseView 分别表示Presenter与View的基类,剩下的utils包用到时再进行查阅即可。

小结

经过简单的查看,不难作出如下推论。

  1. 项目采用MVP模式进行代码组织,每个功能模块均有其专属的 Activity、Fragmeng 与Presenter,所有的功能都围绕 Task 进行 CURD,所以 Mode 层只有一个实体类。
  2. Model 层内涵了加载数据的业务逻辑
  3. Model 层采用了 local 和 remote 双数据源,推断 TasksRepository 拥有缓存策略。

同时还有一些疑问:

  1. M-V-P是怎样组织的?
  2. M-V-P间如何进行通信?

接下来便通过代码来找寻答案

架构组织与通信

架构组织

1. 从最简单的Base接口开始看

1
2
3
4
5
6
7
8
9
//BaseView.java
public interface BaseView<T> {
    // 规定View必须实现setPresenter方法,因此View必然持有Presenter的引用
    void setPresenter(T presenter);
}
//BasePresenter.java    
public interface BasePresenter {
    void start();
}

2. Tasks契约接口

 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
//TasksContract.java
public interface TasksContract {
    interface View extends BaseView<Presenter> {
        //设置加载指示器是否显示
        void setLoadingIndicator(boolean active);
        //显示任务列表
        void showTasks(List<Task> tasks);
        //进入新增任务页
        void showAddTask();
        //进入任务详情页
        void showTaskDetailsUi(String taskId);
        //显示任务被标记为完成状态的提示
        void showTaskMarkedComplete();
        //显示任务被标记为活跃状态的提示
        void showTaskMarkedActive();
        //显示完成状态的任务被清除的提示
        void showCompletedTasksCleared();
        //显示任务加载失败的提示
        void showLoadingTasksError();
        //显示无任务视图
        void showNoTasks();
        //显示无活跃任务视图
        void showNoActiveTasks();
        //显示无完成任务视图
        void showNoCompletedTasks();
        //修改顶部Label文字
        void showActiveFilterLabel();
        void showCompletedFilterLabel();
        void showAllFilterLabel();
        //显示成功保存信息的提示
        void showSuccessfullySavedMessage();
        //判断Fragment是否活跃
        boolean isActive();
        //显示过滤器菜单弹窗
        void showFilteringPopUpMenu();
    }
    interface Presenter extends BasePresenter {
        //Fragment.onActivityResults回调
        void result(int requestCode, int resultCode);
        //加载任务列表
        void loadTasks(boolean forceUpdate);
        //添加新任务
        void addNewTask();
        //打开任务详情
        void openTaskDetails(@NonNull Task requestedTask);
        //标记某任务已完成
        void completeTask(@NonNull Task completedTask);
        //标记某任务未完成
        void activateTask(@NonNull Task activeTask);
        //清除已完成任务
        void clearCompletedTasks();
        //设置过滤器
        void setFiltering(TasksFilterType requestType);
        //获取过滤器
        TasksFilterType getFiltering();
    }
}

TasksContract 采用内部接口的形式将 View 与 Presenter 结合在一处,使得 View 和 Presenter 中有哪些功能看起来一目了然,维护时也很方便。

  • TasksContract.View 声明了任务列表所有的UI动态
  • TasksContract.Presenter 声明了任务列表的所有业务事件

3. 契约接口的实现类

1
2
3
4
//TasksFragment.java
public class TasksFragment extends Fragment implements TasksContract.View {
//TasksPresenter.java
public class TasksPresenter implements TasksContract.Presenter {

TasksFragment 与 TasksPresenter 分别实现了 TasksContract.View 与 TasksContract.Presenter 接口。因此 TasksFragment 对应任务列表模块的 View 层,TasksPresenter 对应任务列表模块的 Presenter 层。

  • TasksFragment 负责 View 层的视图改动,当有用户事件发生或者需要获取数据时,需交由 Presenter 处理
  • TasksPresenter 负责 Presenter 层的事件处理

4. View 与 Presenter 如何建立联系

从TasksActivity入手

tasks_act.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//tasks_act.xml
<androidx.drawerlayout.widget.DrawerLayout>
    <LinearLayout>
        <com.google.android.material.appbar.AppBarLayout />
        <androidx.coordinatorlayout.widget.CoordinatorLayout>
            // Fragment 占位容器
            <FrameLayout
                android:id="@+id/contentFrame"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <com.google.android.material.floatingactionbutton.FloatingActionButton />
        </androidx.coordinatorlayout.widget.CoordinatorLayout>
    </LinearLayout>
    <com.google.android.material.navigation.NavigationView />
</androidx.drawerlayout.widget.DrawerLayout>

TasksActivity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//TasksActivity.java
protected void onCreate(Bundle savedInstanceState) {
    //...
    TasksFragment tasksFragment =
            (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
    if (tasksFragment == null) {
        tasksFragment = TasksFragment.newInstance();
        // 将TasksFragment 关联到 TasksActivity
        ActivityUtils.addFragmentToActivity(
                getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
    }

    // 实例化Presenter时,将repository,fragment作为参数传入
    mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
    //...
}

在TasksActivity的onCreate方法内attach了TasksFragment,同时对TasksPresenter进行实例化,而TasksPresenter的构造函数与tasksFragment有很强的关联。

追踪 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
//TasksPresenter.java
public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
    mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
    //采用断言检测参数是否为Null
    mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
    //为View设置Presenter
    mTasksView.setPresenter(this);
}
//BasePresenter 基类继承而来的方法,一旦被调用便执行 loadTasks
@Override
public void start() {
    loadTasks(false);
}

//TasksFragment.java
@Override
public void onResume() {
    super.onResume();
    //当 Fragment 的生命周期到达 resume 状态时,调用 presenter 的 start 方法
    mPresenter.start();
}

@Override
public void setPresenter(@NonNull TasksContract.Presenter presenter) {
    //采用断言检测参数是否为Null,若非Null则将其赋值给 mPresenter
    mPresenter = checkNotNull(presenter);
}

在TasksPresenter的构造方法中,终于看到 Presenter 层与 View 层是如何关联的,至此,MVP之间的桥梁总算架通,接下来我们将视线转移到层次间的交互上。

组件通信:列表刷新的前世今生

刷新事件发生时,TasksFragment 将其转交给 TasksPresenter 处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//TasksFragment.java

// 用户点击菜单控件,Fragment收到点击事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.menu_clear:
            mPresenter.clearCompletedTasks();
            break;
        case R.id.menu_filter:
            showFilteringPopUpMenu();
            break;
        case R.id.menu_refresh:
            //若用户点击的是刷新按钮,则交由presenter 的 loadTasks 方法进行处理
            mPresenter.loadTasks(true);
            break;
    }
    return true;
}

TasksPresenter 对 loadTastk(true) 的响应

 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
//TasksPrenseter.java

@Override
public void loadTasks(boolean forceUpdate) {
    // loadTasks(true) 时,下条语句可直接看做 loadTasks(true,true);
    loadTasks(forceUpdate || mFirstLoad, true);
    mFirstLoad = false;
}

private void loadTasks(boolean forceUpdate, final boolean showLoadingUI) {
    if (showLoadingUI) {
        //显示加载中动画
        mTasksView.setLoadingIndicator(true);
    }
    if (forceUpdate) {
        //强制更新时,将repo的脏缓存标志设置为true,达到放弃缓存的目的
        mTasksRepository.mCacheIsDirty = true;
    }

    mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
        //数据源成功回调
        @Override
        public void onTasksLoaded(List<Task> tasks) {
            List<Task> tasksToShow = new ArrayList<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;
                }
            }
            // 如果 TasksFragment 不活跃,则中止操作,直接返回
            if (!mTasksView.isActive()) { return; }
            // 关闭加载动画
            if (showLoadingUI) { mTasksView.setLoadingIndicator(false); }
            if (tasks.isEmpty()) {
            // 当列表为空时,根据过滤器状态显示对应的空列表提示信息
                switch (mCurrentFiltering) {
                    case ACTIVE_TASKS:
                        //将View层的变动交由mTasksView即TasksFragment处理
                        mTasksView.showNoActiveTasks();
                        break;
                    case COMPLETED_TASKS:
                        mTasksView.showNoCompletedTasks();
                        break;
                    default:
                        mTasksView.showNoTasks();
                        break;
                }       
            } else {
                // 列表不为空时,调用mTasksView.showTasks来展示最新获取到的任务列表
                mTasksView.showTasks(tasks);
                showFilterLabel();
            }
        }
        //数据源失败回调
        @Override
        public void onDataNotAvailable() {

            if (!mTasksView.isActive()) {
                return;
            }
            //显示加载失败提示
            mTasksView.showLoadingTasksError();
        }
    });
}

TasksPresenter通过TasksRepository获取数据,无论成功或失败,都会通过mTasksView即TasksFragment来完成View的更新。

TasksFragment 对 showTasks 的响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//TasksFragment.java
@Override
public void showTasks(List<Task> tasks) {
    //通知adapter
    mListAdapter.replaceData(tasks);
    //显示任务列表
    mTasksView.setVisibility(View.VISIBLE);
    //隐藏无任务视图
    mNoTasksView.setVisibility(View.GONE);
}
//TasksFragment.TasksAdapter
public void replaceData(List<Task> tasks) {
    mTasks = checkNotNull(tasks);
    //通知ListView更新
    notifyDataSetChanged();
}

TasksRepository 对 getTasks 的响应

1. Repository 的实现

 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
//TasksDataSource.java
public interface TasksDataSource {
    //加载任务列表回调
    interface LoadTasksCallback {
        //成功回调
        void onTasksLoaded(List<Task> tasks);
        //失败回调
        void onDataNotAvailable();
    }
    //获取单个任务回调
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    //获取任务列表
    void getTasks(@NonNull LoadTasksCallback callback);
    //获取单个任务
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    //保存单个任务
    void saveTask(@NonNull Task task);
    //完成单个任务
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    //激活单个任务
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    //清空已完成任务
    void clearCompletedTasks();
    //刷新任务列表
    void refreshTasks();
    //删除所有任务
    void deleteAllTasks();
    //删除单个任务
    void deleteTask(@NonNull String taskId);
}
//TasksRepository.java
public class TasksRepository implements TasksDataSource {
    //单例模式
    private static TasksRepository INSTANCE = null;
    //持有本地数据源与远程数据源
    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;
    //私有化构造方法,屏蔽 new 操作
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }
    //单例方法
    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }

2. Repository 内的缓存策略

 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
//获取任务列表
@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() {
            //若在本地数据库内成功获得数据,则将数据回调给 Presenter
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }
            //若在本地数据库获得失败,则重新从远程获取数据
            @Override
            public void onDataNotAvailable() {
                getTasksFromRemoteDataSource(callback);
            }
        });
    }
}
//从远程获取数据
private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
    mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
        //若成功获得数据,则将数据回调给 Presenter
        @Override
        public void onTasksLoaded(List<Task> tasks) {
            //刷新缓存标志
            refreshCache(tasks);
            //刷新本地数据库
            refreshLocalDataSource(tasks);
            //将数据回调给 Presenter
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
        }
        //若成功获得数据,则将将失败事件回调给 Presenter
        @Override
        public void onDataNotAvailable() {
            callback.onDataNotAvailable();
        }
    });
}

组件作用

Contract 在 MVP 中的作用

Contract 是每个模块的契约,对模块内的 View 和 Presenter 起到提纲擎领的作用,是编写代码前首先需要思考的内容,它的职责如下:

  1. 整合 View 与 Presenter 接口,便于维护
  2. 为 View 与 Presenter 的功能与职责做出规范,便于阅读及理解
  3. 作为 View 和 Presenter 的抽象,是遵循依赖反转原则(Dependency Inversion Principle)的前提

Activity 在 MVP 中的作用

Activity 是每个模块的入口,它的职责如下:

  1. 响应 Actiivty 的生命周期事件
  2. 加载 layout ,响应来自 drawer 及 fab 的用户事件
  3. 实例化 Fragment(View) 及 Presenter,并建立二者间的关联
  4. 为 Presenter 注入 Repository

关于第4点,将在后边的依赖反转章节进行详细阐述。

Fragment 在 MVP 中的作用

Fragment 作为 Fragment 的子类及 Contract.View 接口的实现类,它有以下几点职责:

  1. 响应 Fragment 的生命周期事件
  2. 加载 layout ,完成子控件的实例化及映射,对用户事件进行处理或将其中转给 Contract.Presenter
  3. 作为 Contract.View 接口的具体实现,对 View 层进行更新

Presenter 在 MVP 中的作用

Presenter 仅作为 Contract.Presenter 接口的实现类,它有如下几点职责:

  1. 纯 Java 代码,与 Android SDK 完全解耦,为测试提供便利
  2. 响应 Fragment 中转来的用户事件,对用户事件进行处理或将其中转给 Repository
  3. 响应 Repository 的回调,随后将 Model 层返回的数据中转给 Contract.View

Repository 在 MVP 中的作用

Repository 作为 Model 模块内的组件以及 DataSource 接口的实现类,它的职责如下:

  1. Repository 本身并未存储数据,它仅负责数据的缓存策略
  2. 将来自 Presenter 的调用先进行 CURD 缓存策略的处理,再中转给实际的 LocalDataSource / RemoteDataSource,并将最终结果回调给 Presenter

MVP间的通信方式

小结

  1. 项目的模块划分与代码结构非常清晰,使得项目易于理解与上手
  2. Activity 与 Fragment 的同时使用,带来了更好的分离性,Acitivity 作为模块入口兼控制器,负责view、presenter的创建与连接
  3. UI代码与业务代码进行了拆分,整体的可测试性非常好,UI层和业务层均可单独进行测试。

设计模式

面向对象设计的 S.O.L.I.D 原则

缩写名称描述
SRP单一职责原则(Singel responsibility principle)每个类、接口尽可能的只负责单方面的工作
OCP开闭原则(Open-closed princile)对扩展开放,对修改关闭
LSP里式替换原则(Liskov substitution princile)程序中的对象可以在不改变其正确性的条件下替换为它的子类对象
ISP接口隔离原则(Interface segregation princile)尽量使用多个小而精接口代替一个大而多的超级接口,从而在接口改动时减少对上层调用者的影响
DIP依赖反转原则(Dependency inversion princile)因为里式替换原则的存在,鼓励对象间的引用尽可能的依赖抽象而不是细节(即尽可能的使用父类型作为对象的引用类型),以此来降低依赖时的耦合度

todo-mvp 中设计原则的体现

  1. View 与 Presenter 相互持有时,都使用父类对象作为引用,满足了里式替换原则和依赖反转原则
  2. Model、View 与 Presenter 间的划分很清晰,满足了接口隔离原则与单一职责原则
  3. Repository、LocalDataSource、RemoteDataSource 对 DataSource 的不同实现满足了开闭原则

一个依赖注入的细节

TasksActivity 的 onCreate 在创建 TasksPresenter 时,通过 Injection 来获取 Repository。Injection 通常伴随强烈的依赖注入语义,此处必有蹊跷

1
2
3
4
5
6
7
8
//TasksActivity.java onCreate
protected void onCreate(Bundle savedInstanceState) {
    //...
    mTasksPresenter = new TasksPresenter(
            Injection.provideTasksRepository(getApplicationContext()), 
            tasksFragment);
    //...
}

回到项目组织,在 mock 与 prod 下,果然看到两个同名的 Injection 类 TodoMVP 项目目录 除 RemoteDataSourcew 外,两份Injection的代码完全一致,甚至连包名都一致

 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
// src/mock 
package com.example.android.architecture.blueprints.todoapp;
public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        // mock 环境使用 FakeTasksRemoteDataSource
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}
// src/prod
package com.example.android.architecture.blueprints.todoapp;
public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        // prod 环境使用 TasksRemoteDataSource
        return TasksRepository.getInstance(TasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}

回到项目组织,打开Build Variant窗口,同时打开app下的 build.gradle文件

TodoMVP 项目目录

根据 mock/java 的蓝色文件夹图标可知,mock 下的 Injection.java 为当前源码集的一部分,项目编译时运行时会选用这个 Injection

我们在Build Variant窗口的列表中,使用鼠标选中 prodDebug或prodRelease ,项目变回会自动进行 Gradle Sync 操作

Executing tasks: [:app:generateProdDebugSources]

> Configure project :
...省略
> Task :app:preProdDebugBuild UP-TO-DATE
> Task :app:compileProdDebugAidl NO-SOURCE
> Task :app:compileProdDebugRenderscript UP-TO-DATE
> Task :app:checkProdDebugManifest UP-TO-DATE
> Task :app:generateProdDebugBuildConfig UP-TO-DATE
> Task :app:generateProdDebugSources UP-TO-DATE

BUILD SUCCESSFUL in 0s
6 actionable tasks: 6 up-to-date

执行完成后再来观察项目组织,发现 prod/java 成为了源码集的一部分,mock/java 则变为了普通文件夹 TodoMVP 项目目录 此时若进行编译,便会将 prod/java 下的 Injection 编译到项目中,而 mock 下的文件夹则会被编译器忽略。

小结

Injection 类通过 build vartiant 改变源码集的方式来实现注入,避免了手动修改带来的潜在问题,使得 mock 与 prod 环境间的切换变得尤为方便,不失为一种优雅的方式。

后记

  1. 架构出现的目的是降低项目复杂度
  2. 降低项目复杂度的手段是尽可能的解耦
  3. 解耦的思路是运用设计模式来组织代码
  4. 设计模式遵循的原则是 SOLID

参考链接: Android官方MVP架构项目解析 SOLID 【第二章】 IoC 之 2.1 IoC基础 ——跟我学Spring3 Leveraging product flavors in Android Studio for hermetic testing