Google官方MVP Sample代码解读

关于Android程序的构架, 当前最流行的模式即为MVP模式, Google官方提供了Sample代码来展示这种模式的用法.
Repo地址: android-architecture.
本文为阅读官方sample代码的阅读笔记和分析.

官方Android Architecture Blueprints [beta]:
Android在如何组织和构架一个app方面提供了很大的灵活性, 但是同时这种自由也可能会导致app在测试, 维护, 扩展方面变得困难.

Android Architecture Blueprints展示了可能的解决方案. 在这个项目里, 我们用各种不同的构架概念和工具实现了同一个应用(To Do App). 主要的关注点在于代码结构, 构架, 测试和维护性.
但是请记住, 用这些模式构架app的方式有很多种, 要根据你的需要, 不要把这些当做绝对的典范.

MVP模式 概念

之前有一个MVC模式: Model-View-Controller.
MVC模式 有两个主要的缺点: 首先, View持有Controller和Model的引用; 第二, 它没有把对UI逻辑的操作限制在单一的类里, 这个职能被Controller和View或者Model共享.
所以后来提出了MVP模式来克服这些缺点.

MVP(Model-View-Presenter)模式:

  • Model: 数据层. 负责与网络层和数据库层的逻辑交互.
  • View: UI层. 显示数据, 并向Presenter报告用户行为.
  • Presenter: 从Model拿数据, 应用到UI层, 管理UI的状态, 决定要显示什么, 响应用户的行为.
    MVP模式的最主要优势就是耦合降低, Presenter变为纯Java的代码逻辑, 不再与Android Framework中的类如Activity, Fragment等关联, 便于写单元测试.

todo-mvp 基本的Model-View-Presenter架构

app中有四个功能:

  • Tasks
  • TaskDetail
  • AddEditTask
  • Statistics

每个功能都有:

  • 一个定义View和Presenter接口的Contract接口;
  • 一个Activity用来管理fragment和presenter的创建;
  • 一个实现了View接口的Fragment;
  • 一个实现了Presenter接口的presenter.

mvp

基类

Presenter基类:

public interface BasePresenter {    void start();}

例子中这个start()方法都在Fragment的onResume()中调用.

View基类:

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

View实现

  • Fragment作为每一个View接口的实现, 主要负责数据显示和在用户交互时调用Presenter, 但是例子代码中也是有一些直接操作的部分, 比如点击开启另一个Activity, 点击弹出菜单(菜单项的点击仍然是调用presenter的方法).
  • View接口中定义的方法多为showXXX()方法.

  • Fragment作为View实现, 接口中定义了方法:

    @Overridepublic boolean isActive() {  return isAdded();}

在Presenter中数据回调的方法中, 先检查View.isActive()是否为true, 来保证对Fragment的操作安全.

Presenter实现

  • Presenter的start()方法在onResume()的时候调用, 这时候取初始数据; 其他方法均对应于用户在UI上的交互操作.
  • New Presenter的操作是在每一个Activity的onCreate()里做的: 先添加了Fragment(View), 然后把它作为参数传给了Presenter. 这里并没有存Presenter的引用.
  • Presenter的构造函数有两个参数, 一个是Model(Model类一般叫XXXRepository), 一个是View. 构造中先用guava的checkNotNull()
    检查两个参数是否为null, 然后赋值到字段; 之后再调用View的setPresenter()方法把Presenter传回View中引用.

Model实现细节

  • Model只有一个类, 即TasksRepository. 它还是一个单例. 因为在这个应用的例子中, 我们操作的数据就这一份.

它由手动实现的注入类Injection类提供:

public class Injection {    public static TasksRepository provideTasksRepository(@NonNull Context context) {        checkNotNull(context);        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),                TasksLocalDataSource.getInstance(context));    }}

构造如下:

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,                        @NonNull TasksDataSource tasksLocalDataSource) {    mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);    mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);}
  • 数据分为local和remote两大部分. local部分负责数据库的操作, remote部分负责网络. Model类中还有一个内存缓存.
  • TasksDataSource是一个接口. 接口中定义了Presenter查询数据的回调接口, 还有一些增删改查的方法.

单元测试

MVP模式的主要优势就是便于为业务逻辑加上单元测试.
本例子中的单元测试是给TasksRepository和四个feature的Presenter加的.
Presenter的单元测试, Mock了View和Model, 测试调用逻辑, 如:

public class AddEditTaskPresenterTest {    @Mock    private TasksRepository mTasksRepository;    @Mock    private AddEditTaskContract.View mAddEditTaskView;    private AddEditTaskPresenter mAddEditTaskPresenter;    @Before    public void setupMocksAndView() {        MockitoAnnotations.initMocks(this);        when(mAddEditTaskView.isActive()).thenReturn(true);    }    @Test    public void saveNewTaskToRepository_showsSuccessMessageUi() {        mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView);        mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Description");        verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model        verify(mAddEditTaskView).showTasksList(); // shown in the UI    }    ...}

todo-mvp-loaders 用Loader取数据的MVP

基于上一个例子todo-mvp, 只不过这里改为用Loader来从Repository得到数据.


todo-mvp-loaders

使用Loader的优势:

  • 去掉了回调, 自动实现数据的异步加载;
  • 当内容改变时回调出新数据;
  • 当应用因为configuration变化而重建loader时, 自动重连到上一个loader.

Diff with todo-mvp

既然是基于todo-mvp, 那么之前说过的那些就不再重复, 我们来看一下都有什么改动:
git difftool -d todo-mvp

添加了两个类:
TaskLoaderTasksLoader.

在Activity中new Loader类, 然后传入Presenter的构造方法.

Contract中View接口删掉了isActive()方法, Presenter删掉了populateTask()方法.

数据获取

添加的两个新类是TaskLoaderTasksLoader, 都继承于AsyncTaskLoader, 只不过数据的类型一个是单数, 一个是复数.

AsyncTaskLoader是基于ModernAsyncTask, 类似于AsyncTask,
把load数据的操作放在loadInBackground()里即可, deliverResult()方法会将结果返回到主线程, 我们在listener的onLoadFinished()里面就可以接到返回的数据了, (在这个例子中是几个Presenter实现了这个接口).

TasksDataSource接口的这两个方法:

List getTasks();Task getTask(@NonNull String taskId);

都变成了同步方法, 因为它们是在loadInBackground()方法里被调用.

Presenter中保存了LoaderLoaderManager, 在start()方法里initLoader, 然后onCreateLoader返回构造传入的那个loader.
onLoadFinished()里面调用View的方法. 此时Presenter实现LoaderManager.LoaderCallbacks.

数据改变监听

TasksRepository类中定义了observer的接口, 保存了一个listener的list:

private List mObservers = new ArrayList();public interface TasksRepositoryObserver {    void onTasksChanged();}

每次有数据改动需要刷新UI时就调用:

private void notifyContentObserver() {    for (TasksRepositoryObserver observer : mObservers) {        observer.onTasksChanged();    }}

在两个Loader里注册和注销自己为TasksRepository的listener: 在onStartLoading()里add, onReset()里面remove方法.
这样每次TasksRepository有数据变化, 作为listener的两个Loader都会收到通知, 然后force load:

@Overridepublic void onTasksChanged() {    if (isStarted()) {        forceLoad();    }}

这样onLoadFinished()方法就会被调用.

todo-databinding

基于todo-mvp, 使用Data Binding library来显示数据, 把UI和动作绑定起来.

说到ViewModel, 还有一种模式叫MVVM(Model-View-ViewModel)模式.
这个例子并没有严格地遵循Model-View-ViewModel模式或者Model-View-Presenter模式, 因为它既用了ViewModel又用了Presenter.


mvp-databinding

Data Binding Library让UI元素和数据模型绑定:

  • layout文件用来绑定数据和UI元素;
  • 事件和action handler绑定;
  • 数据变为可观察的, 需要的时候可以自动更新.

Diff with todo-mvp

添加了几个类:

  • StatisticsViewModel;
  • SwipeRefreshLayoutDataBinding;
  • TasksItemActionHandler;
  • TasksViewModel;

从几个View的接口可以看出方法数减少了, 原来需要多个showXXX()方法, 现在只需要一两个方法就可以了.

数据绑定

TasksDetailFragment为例:
以前在todo-mvp里需要这样:

public void onCreateView(...) {    ...    mDetailDescription = (TextView)root.findViewById(R.id.task_detail_description);}@Overridepublic void showDescription(String description) {    mDetailDescription.setVisibility(View.VISIBLE);    mDetailDescription.setText(description);}

现在只需要这样:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {    View view = inflater.inflate(R.layout.taskdetail_frag, container, false);    mViewDataBinding = TaskdetailFragBinding.bind(view);    ...}@Overridepublic void showTask(Task task) {    mViewDataBinding.setTask(task);}

因为所有数据绑定的操作都写在了xml里:

<TextView    android:id="@+id/task_detail_description"    ...    android:text="@{task.description}" />

事件绑定

数据绑定省去了findViewById()setText(), 事件绑定则是省去了setOnClickListener().

比如taskdetail_frag.xml中的

<CheckBox    android:id="@+id/task_detail_complete"    ...    android:checked="@{task.completed}"    android:onCheckedChanged="@{(cb, isChecked) ->    presenter.completeChanged(task, isChecked)}" />

其中Presenter是这时候传入的:

@Overridepublic void onActivityCreated(Bundle savedInstanceState) {    super.onActivityCreated(savedInstanceState);    mViewDataBinding.setPresenter(mPresenter);}

数据监听

在显示List数据的界面TasksFragment, 仅需要知道数据是否为空, 所以它使用了TasksViewModel来给layout提供信息, 当尺寸设定的时候, 只有一些相关的属性被通知, 和这些属性绑定的UI元素被更新.

public void setTaskListSize(int taskListSize) {    mTaskListSize = taskListSize;    notifyPropertyChanged(BR.noTaskIconRes);    notifyPropertyChanged(BR.noTasksLabel);    notifyPropertyChanged(BR.currentFilteringLabel);    notifyPropertyChanged(BR.notEmpty);    notifyPropertyChanged(BR.tasksAddViewVisible);}

其他实现细节

  • Adapter中的Data Binding, 见TasksFragment中的TasksAdapter.

    @Overridepublic View getView(int i, View view, ViewGroup viewGroup) {  Task task = getItem(i);  TaskItemBinding binding;  if (view == null) {      // Inflate      LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());      // Create the binding      binding = TaskItemBinding.inflate(inflater, viewGroup, false);  } else {      binding = DataBindingUtil.getBinding(view);  }  // We might be recycling the binding for another task, so update it.  // Create the action handler for the view  TasksItemActionHandler itemActionHandler =          new TasksItemActionHandler(mUserActionsListener);  binding.setActionHandler(itemActionHandler);  binding.setTask(task);  binding.executePendingBindings();  return binding.getRoot();}
  • Presenter可能会被包在ActionHandler中, 比如TasksItemActionHandler.
  • ViewModel也可以作为View接口的实现, 比如StatisticsViewModel.
  • SwipeRefreshLayoutDataBinding类定义的onRefresh()动作绑定.

todo-mvp-clean

这个例子是基于Clean Architecture的原则:
The Clean Architecture.
关于Clean Architecture, 还可以看这个Sample App: Android-CleanArchitecture.

这个例子在todo-mvp的基础上, 加了一层domain层, 把应用分为了三层:


mvp-clean.png

Domain: 盛放了业务逻辑, domain层包含use cases或者interactors, 被应用的presenters使用. 这些use cases代表了所有从presentation层可能进行的行为.

关键概念
和基本的mvp sample最大的不同就是domain层和use cases. 从presenters中抽离出来的domain层有助于避免presenter中的代码重复.

Use cases定义了app需要的操作, 这样增加了代码的可读性, 因为类名反映了目的.

Use cases对于操作的复用来说也很好. 比如CompleteTask在两个Presenter中都用到了.

Use cases的执行是在后台线程, 使用command pattern. 这样domain层对于Android SDK和其他第三方库来说都是完全解耦的.

Diff with todo-mvp

每一个feature的包下都新增了domain层, 里面包含了子目录model和usecase等.

UseCase是一个抽象类, 定义了domain层的基础接口点.
UseCaseHandler用于执行use cases, 是一个单例, 实现了command pattern.
UseCaseThreadPoolScheduler实现了UseCaseScheduler接口, 定义了use cases执行的线程池, 在后台线程异步执行, 最后把结果返回给主线程.
UseCaseScheduler通过构造传给UseCaseHandler.
测试中用了UseCaseScheduler的另一个实现TestUseCaseScheduler, 所有的执行变为同步的.

Injection类中提供了多个Use cases的依赖注入, 还有UseCaseHandler用来执行use cases.

Presenter的实现中, 多个use cases和UsseCaseHandler都由构造传入, 执行动作, 比如更新一个task:

private void updateTask(String title, String description) {    if (mTaskId == null) {        throw new RuntimeException("updateTask() was called but task is new.");    }    Task newTask = new Task(title, description, mTaskId);    mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask),            new UseCase.UseCaseCallback() {                @Override                public void onSuccess(SaveTask.ResponseValue response) {                    // After an edit, go back to the list.                    mAddTaskView.showTasksList();                }                @Override                public void onError() {                    showSaveError();                }            });}

todo-mvp-dagger

关键概念:
dagger2 是一个静态的编译期依赖注入框架.
这个例子中改用dagger2实现依赖注入. 这样做的主要好处就是在测试的时候我们可以用替代的modules. 这在编译期间通过flavors就可以完成, 或者在运行期间使用一些调试面板来设置.

Diff with todo-mvp

Injection类被删除了.
添加了5个Component, 四个feature各有一个, 另外数据对应一个: TasksRepositoryComponent, 这个Component被保存在Application里.

数据的module: TasksRepositoryModulemockprod目录下各有一个.

对于每一个feature的Presenter的注入是这样实现的:
首先, 把Presenter的构造函数标记为@Inject, 然后在Activity中构造component并注入到字段:

@Inject AddEditTaskPresenter mAddEditTasksPresenter;@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.addtask_act);    .....    // Create the presenter    DaggerAddEditTaskComponent.builder()            .addEditTaskPresenterModule(                    new AddEditTaskPresenterModule(addEditTaskFragment, taskId))            .tasksRepositoryComponent(                    ((ToDoApplication) getApplication()).getTasksRepositoryComponent()).build()            .inject(this);}

这个module里provide了view和taskId:

@Modulepublic class AddEditTaskPresenterModule {    private final AddEditTaskContract.View mView;    private String mTaskId;    public AddEditTaskPresenterModule(AddEditTaskContract.View view, @Nullable String taskId) {        mView = view;        mTaskId = taskId;    }    @Provides    AddEditTaskContract.View provideAddEditTaskContractView() {        return mView;    }    @Provides    @Nullable    String provideTaskId() {        return mTaskId;    }}

注意原来构造方法里调用的setPresenter方法改为用方法注入实现:

/** * Method injection is used here to safely reference {@code this} after the object is created. * For more information, see Java Concurrency in Practice. */@Injectvoid setupListeners() {    mAddTaskView.setPresenter(this);}

todo-mvp-contentproviders

这个例子是基于todo-mvp-loaders的, 用content provider来获取repository中的数据.


mvp-contentproviders

使用Content Provider的优势是:

  • 管理了结构化数据的访问;
  • Content Provider是跨进程访问数据的标准接口.

Diff with todo-mvp-loaders

注意这个例子是唯一一个不基于最基本的todo-mvp, 而是基于todo-mvp-loaders. (但是我觉得也可以认为是直接从todo-mvp转化的.)
看diff: git difftool -d todo-mvp-loaders.

去掉了TaskLoaderTasksLoader. (回归到了基本的todo-mvp).

TasksRepository中的方法不是同步方法, 而是异步加callback的形式. (回归到了基本的todo-mvp).

TasksLocalDataSource中的读方法都变成了空实现, 因为Presenter现在可以自动收到数据更新.

新增LoaderProvider用来创建Cursor Loaders, 有两个方法:

// 返回特定fiter下或全部的数据public Loader createFilteredTasksLoader(TaskFilter taskFilter)// 返回特定id的数据public Loader createTaskLoader(String taskId)

其中第一个方法的参数TaskFilter, 用来指定过滤的selection条件, 也是新增类.

LoaderManagerLoaderProvider都是由构造传入Presenter, 在回调onTaskLoaded()onTasksLoaded()中init loader.

TasksPresenter中还做了判断, 是init loader还是restart loader:

@Overridepublic void onTasksLoaded(List tasks) {    // we don't care about the result since the CursorLoader will load the data for us    if (mLoaderManager.getLoader(TASKS_LOADER) == null) {        mLoaderManager.initLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this);    } else {        mLoaderManager.restartLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this);    }}

其中initLoader()和restartLoader()时传入的第二个参数是一个bundle, 用来指明过滤类型, 即是带selection条件的数据库查询.

同样是在onLoadFinshed()的时候做View处理, 以TaskDetailPresenter为例:

@Overridepublic void onLoadFinished(Loader loader, Cursor data) {    if (data != null) {        if (data.moveToLast()) {            onDataLoaded(data);        } else {            onDataEmpty();        }    } else {        onDataNotAvailable();    }}

数据类Task中新增了静态方法从Cursor转为Task, 这个方法在Presenter的onLoadFinished()和测试中都用到了.

public static Task from(Cursor cursor) {    String entryId = cursor.getString(cursor.getColumnIndexOrThrow(            TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID));    String title = cursor.getString(cursor.getColumnIndexOrThrow(            TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE));    String description = cursor.getString(cursor.getColumnIndexOrThrow(            TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION));    boolean completed = cursor.getInt(cursor.getColumnIndexOrThrow(            TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED)) == 1;    return new Task(title, description, entryId, completed);}

另外一些细节:
数据库中的内存cache被删了.
Adapter改为继承于CursorAdapter.

单元测试

新增了MockCursorProvider类, 用于在单元测试中提供数据.
其内部类TaskMockCursor mock了Cursor数据.
Presenter的测试中仍然mock了所有构造传入的参数, 然后准备了mock数据, 测试的逻辑主要还是拿到数据后的view操作, 比如:

@Testpublic void loadAllTasksFromRepositoryAndLoadIntoView() {    // When the loader finishes with tasks and filter is set to all    when(mBundle.getSerializable(TaskFilter.KEY_TASK_FILTER)).thenReturn(TasksFilterType.ALL_TASKS);    TaskFilter taskFilter = new TaskFilter(mBundle);    mTasksPresenter.setFiltering(taskFilter);    mTasksPresenter.onLoadFinished(mock(Loader.class), mAllTasksCursor);    // Then progress indicator is hidden and all tasks are shown in UI    verify(mTasksView).setLoadingIndicator(false);    verify(mTasksView).showTasks(mShowTasksArgumentCaptor.capture());}

todo-mvp-rxjava

关于这个例子, 之前看过作者的文章: Android Architecture Patterns Part 2:
Model-View-Presenter,
这个文章上过Android Weekly Issue #226.

这个例子也是基于todo-mvp, 使用RxJava处理了presenter和数据层之间的通信.

MVP基本接口改变

BasePresenter接口改为:

public interface BasePresenter {    void subscribe();    void unsubscribe();}

View在onResume()的时候调用Presenter的subscribe(); 在onPause()的时候调用presenter的unsubscribe().

如果View接口的实现不是Fragment或Activity, 而是Android的自定义View, 那么在Android View的onAttachedToWindow()onDetachedFromWindow()方法里分别调用这两个方法.

Presenter中保存了:

private CompositeSubscription mSubscriptions;

subscribe()的时候, mSubscriptions.add(subscription);;
unsubscribe()的时候, mSubscriptions.clear(); .

Diff with todo-mvp

数据层暴露了RxJava的Observable流作为获取数据的方式, TasksDataSource接口中的方法变成了这样:

Observable> getTasks();Observable getTask(@NonNull String taskId);

callback接口被删了, 因为不需要了.

TasksLocalDataSource中的实现用了SqlBrite, 从数据库中查询出来的结果很容易地变成了流:

@Overridepublic Observable> getTasks() {    ...    return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)            .mapToList(mTaskMapperFunction);}

TasksRepository中整合了local和remote的data, 最后把Observable返回给消费者(Presenters和Unit Tests). 这里用了.concat().first()操作符.

Presenter订阅TasksRepository的Observable, 然后决定View的操作, 而且Presenter也负责线程的调度.
简单的比如AddEditTaskPresenter中:

@Overridepublic void populateTask() {    if (mTaskId == null) {        throw new RuntimeException("populateTask() was called but task is new.");    }    Subscription subscription = mTasksRepository            .getTask(mTaskId)            .subscribeOn(mSchedulerProvider.computation())            .observeOn(mSchedulerProvider.ui())            .subscribe(new Observer() {                @Override                public void onCompleted() {                }                @Override                public void onError(Throwable e) {                    if (mAddTaskView.isActive()) {                        mAddTaskView.showEmptyTaskError();                    }                }                @Override                public void onNext(Task task) {                    if (mAddTaskView.isActive()) {                        mAddTaskView.setTitle(task.getTitle());                        mAddTaskView.setDescription(task.getDescription());                    }                }            });    mSubscriptions.add(subscription);}

StatisticsPresenter负责统计数据的显示, TasksPresenter负责过滤显示所有数据, 里面的RxJava操作符运用比较多, 可以看到链式操作的特点.

关于线程调度, 定义了BaseSchedulerProvider接口, 通过构造函数传给Presenter, 然后实现用SchedulerProvider, 测试用ImmediateSchedulerProvider. 这样方便测试.

更多相关文章

  1. Android模拟器RAM修改方法 - 尤其是3.0
  2. Android(安卓)消息机制之Message
  3. Android(安卓)ViewPager使用详解
  4. Android使用setCustomTitle()方法自定义对话框标题
  5. Android(安卓)Content Provider的应用
  6. android笔记--android的进程与线程
  7. android 静默安装,含获取各种应用信息方法,根据apk获取应用信息
  8. Android分包MultiDex源码分析
  9. Android注解支持(Support Annotations)详解

随机推荐

  1. Android第三方开源NiftyNotification(Andr
  2. Android编译系统
  3. android底部标签页的tab实现
  4. Android(安卓)Dialog用法总结
  5. EditText属性大全
  6. Android布局之RelativeLayout相对布局
  7. Android(安卓)智能聊天机器人demo(类似小
  8. Android(安卓)之 EditText属性用法介绍
  9. Android里子线程真的不能刷新UI吗?
  10. android intent 常用用法