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,并为此项目中的其他示例提供基础,旨在:
该项目使用以下命名约定:
在你探索该项目之前,熟悉以下的内容会很有用:
本 APP 使用以下依赖:
蓝图中所有的 todo App 都包含相同的功能:
在这个基础版本以及其他以他为基础的拓展版本当中,每个界面都使用以下类和接口来实现:
Presenter 通常承载着相关的业务逻辑。view 处理 UI 工作,在 view 当中几乎没有逻辑,它只是根据 Presenter 的指令去操作 UI 并且监听用户的操作然后将其传递给 Presenter。
每个版本的都使用不同的方法去实现相同的功能,以展示和对比各种架构设计。例如,该版本采用以下方法来解决常见的问题:
在下面的例子中,这个版本使用 Fragment,原因如下:
该版本的应用程序包含很多单元测试,涵盖了 Presenter、存储库和数据源。该版本还包括依赖于假数据的 UI 测试,并且通过依赖注入提供了假的模块。有关使用依赖注入来测试的更多信息,请参阅 Leveraging product flavors in Android Studio for hermetic testing
上面翻译了项目的 readme,下面我们逐步分析一下这个项目,从而一探究竟解开 MVP 的面纱。
整个项目结构如下图:
其中:
测试内容我们后面再说,先看 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 相关的文章再说吧
当当当当,整个代码阶段解释完毕,其实看着复杂,实际上逻辑很简单对吧。掰掰~