谷歌官方文档 todo-mvp 翻译+代码解释

Google 官方示例中给出了一系列不同架构例子,以供开发者们根据自己的项目选择适合的架构。

Google 把这个项目称为“Android 架构蓝图”,项目地址:https://github.com/googlesamples/android-architecture,在这个蓝图当中,有最基础的 MVP 架构,也有基于 MVP+各种第三方库实现的示例,我们从最基本的 MVP 架构开始。


该示例使用最基本的 MVP 架构实现了一个 APP:TODO。官方介绍在这里 To do app specification

用户可以创建、阅读、更新和删除任务,任务由标题和说明组成,任务分为“完成”和“未完成”状态,完成状态的任务可以统一删除。


todo-mvp README
该 APP 名为 todo-mvp,并为此项目中的其他示例提供基础,旨在:

  • 提供最基本的 MVP 架构。
  • 为其他项目提供参考点和比较对照样本。

该项目使用以下命名约定:


你需要做什么

在你探索该项目之前,熟悉以下的内容会很有用:

本 APP 使用以下依赖:


设计 APP

蓝图中所有的 todo App 都包含相同的功能:

  • Task:用于管理任务列表
  • TaskDetail:用于阅读或删除任务
  • AddEditTask:用于创建或编辑任务
  • Statistics:显示任务相关的统计信息

在这个基础版本以及其他以他为基础的拓展版本当中,每个界面都使用以下类和接口来实现:

  • 一个用于定义 View 层和 Presenter 之间联系的契约类
  • 一个用于创建 Fragment 和 Presenter 的 Activity
  • 一个实现了视图接口(view interface)的 Fragment
  • 一个实现了相应契约的 Presenter interface 的 Presenter

Presenter 通常承载着相关的业务逻辑。view 处理 UI 工作,在 view 当中几乎没有逻辑,它只是根据 Presenter 的指令去操作 UI 并且监听用户的操作然后将其传递给 Presenter。


App 的实现

每个版本的都使用不同的方法去实现相同的功能,以展示和对比各种架构设计。例如,该版本采用以下方法来解决常见的问题:

  • 本例当中在编译时使用 配置构建变体来替换模块,为手动和自动测试提供模拟数据。
  • 该版本使用回调函数来处理异步任务。
  • 数据使用 Room 存储在本地 SQLite 当中。

在下面的例子中,这个版本使用 Fragment,原因如下:

  • 同时使用 Activity 和 Fragment 可以更好的将关心的问题分离开,这与 MVP 的实现相辅相成。在该版本中 Activity 是创建和连接 view 和 Presenter 的总控制器。
  • Fragment 支持多个 view 的 布局或者视图片段。

该版本的应用程序包含很多单元测试,涵盖了 Presenter、存储库和数据源。该版本还包括依赖于假数据的 UI 测试,并且通过依赖注入提供了假的模块。有关使用依赖注入来测试的更多信息,请参阅 Leveraging product flavors in Android Studio for hermetic testing


一探究竟

上面翻译了项目的 readme,下面我们逐步分析一下这个项目,从而一探究竟解开 MVP 的面纱。

项目结构

整个项目结构如下图:

其中:

  • com:app src 目录
  • com(androidTest):UI 层测试
  • com(androidTestMock):UI 层测试 mock 数据支持
  • com(mock):业务层单元测试 mock 数据支持
  • com(test):业务层单元测试

测试内容我们后面再说,先看 src 目录,结构如下图:

先两个基类:

BasePresenter

public interface BasePresenter {
    void start();
}

BaseView

public interface BaseView<T> {
    void setPresenter(T presenter);
}

这两个基类都是接口,BasePresenter 当中含有 start 方法,BaseView 当中含有 setPresenter 方法,参数为一个 Presenter 对象,自然是将 Presenter 传入 view 当中去了。


在该项目当中还有一个“契约类”:TaskDetailContract

public interface TaskDetailContract {
    interface View extends BaseView<Presenter> {
        void setLoadingIndicator(boolean active);
        void showMissingTask();
        void hideTitle();
        void showTitle(String title);
        void hideDescription();
        void showDescription(String description);
        void showCompletionStatus(boolean complete);
        void showEditTask(String taskId);
        void showTaskDeleted();
        void showTaskMarkedComplete();
        void showTaskMarkedActive();
        boolean isActive();
    }
    interface Presenter extends BasePresenter {
        void editTask();
        void deleteTask();
        void completeTask();
        void activateTask();
    }
}

在这个契约类中可以一目了然的看到 view 和 presenter 当中都包含哪些方法或者功能。


接下来我们看入口 Activity,查看清单文件 AndroidManifest.xml 文件可以看到入口 Activity 是 TasksActivity

public class TasksActivity extends AppCompatActivity {

    private static final String CURRENT_FILTERING_KEY = "CURRENT_FILTERING_KEY";

    private DrawerLayout mDrawerLayout;

    private TasksPresenter mTasksPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tasks_act);

        // 设置 toolbar
        ...

        // 设置侧滑导航菜单 navigation drawer.
        ...

        //创建 Fragment
        TasksFragment tasksFragment =
                (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (tasksFragment == null) {
            // Create the fragment
            tasksFragment = TasksFragment.newInstance();
            ActivityUtils.addFragmentToActivity(
                    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
        }

        // 创建 Presenter
        mTasksPresenter = new TasksPresenter(
                Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

        // 加载之前保存过的状态(如果有的话)
        if (savedInstanceState != null) {
            TasksFilterType currentFiltering =
                    (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
            mTasksPresenter.setFiltering(currentFiltering);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        //保存数据
        ...
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        //toolbar 菜单按钮
    }

    //设置 侧滑抽屉菜单
    private void setupDrawerContent(NavigationView navigationView) {
        ...
    }

    //测试
    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}

就跟上面说过的一样,Activity 在这里作为一个控制器,用于创建 view 和 Presenter,Fragment 担任 view 的角色,我们看一下这个 Activity 创建的 Fragment:TasksFragment

// 首先,继承了契约类当中的 view 接口
public class TasksFragment extends Fragment implements TasksContract.View {

    private TasksContract.Presenter mPresenter;

    private TasksAdapter mListAdapter;

    private View mNoTasksView;

    private ImageView mNoTaskIcon;

    private TextView mNoTaskMainView;

    private TextView mNoTaskAddView;

    private LinearLayout mTasksView;

    private TextView mFilteringLabelView;

    public TasksFragment() {
        // Requires empty public constructor
    }

    public static TasksFragment newInstance() {
        return new TasksFragment();
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mListAdapter = new TasksAdapter(new ArrayList<Task>(0), mItemListener);
    }

    // 重点二:这里调用了 Presenter 的 start 方法
    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

    // 重点一:通过这个方法,view 获取到了 Presenter 实例
    @Override
    public void setPresenter(@NonNull TasksContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }

    // 都是一些 Fragment 当中的初始化控件,数据加载,菜单监听等内容,省略
    ...

    // 重点三:接口,定义点击 item 时候的操作
    public interface TaskItemListener {

        void onTaskClick(Task clickedTask);

        void onCompleteTaskClick(Task completedTask);

        void onActivateTaskClick(Task activatedTask);
    }

}

在上面的代码当中,我们看到 view 获取了 Presenter 示例,并且调用了 Presenter 的start 方法,我们再看一下 Presenter:

// 实现契约类当中的 Presenter 接口
public class TasksPresenter implements TasksContract.Presenter {

    private final TasksRepository mTasksRepository;

    private final TasksContract.View mTasksView;

    private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;

    private boolean mFirstLoad = true;

    public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {
        mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
        // Presenter 拿到 view
        mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");
        // 构造方法当中,调用 view 的 setPresenter 方法,将自身作为参数,view 拿到 Presenter
        mTasksView.setPresenter(this);
    }

    // start 方法开始处理数据的处理和加载等操作
    @Override
    public void start() {
        loadTasks(false);
    }
    // 下面一系列操作 UI 的逻辑,Presenter 可以直接通过调用 view 的方法,去操作 UI
    ...
}

思路也相当的清晰了,在构造方法当中调用 view 的 setPresenter 方法,将自身传入。

捋一下整个逻辑:

Activity 作为控制器,创建了 view(Fragment),又创建了 Presenter,并将 view 作为参数传入,在 Presenter 当中调用了 view 的 setPresenter 方法(参数为this),这样 view 就拿到了 Presenter,在 view 的 onResume 方法当中调用了 Presenter 的 start 方法,开始数据操作,整个一套逻辑下来,是不是和 view 和 model 层一点儿关系也没有?这也解释了之前说过的 MVP 当中 M 和 V 层的完全解耦。

Model 层呢?
在这个项目当中,Presenter 的构造方法当中有一行代码:

mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");

在 Activity 当中 tasksRepository 是这样获取的:

// Create the presenter
        mTasksPresenter = new TasksPresenter(
                Injection.provideTasksRepository(getApplicationContext()), tasksFragment);

提取出来就是:

Injection.provideTasksRepository(getApplicationContext())

看一下这个方法:

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}

查看代码发现是通过 Room 进行的数据库操作,实际上还是那点儿功能,数据的获取、操作、转换功能都是在这里实现的,篇幅问题,就不单独拿出来讲了,回头写一个 Room 相关的文章再说吧

当当当当,整个代码阶段解释完毕,其实看着复杂,实际上逻辑很简单对吧。掰掰~

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1