什么是android mvp模式,android mvp模式有什么弊端

什么是android mvp模式,android mvp模式有什么弊端,第1张

  什么是android mvp模式

  关于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.

  什么是android mvp模式,android mvp模式有什么弊端,什么是android mvp模式,android mvp模式有什么弊端,第2张

  mvp

  基类

  Presenter基类:

  public interface BasePresenter {

  void start();

  }

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

  View基类:

  public interface BaseView《T》 {

  void setPresenter(T presenter);

  }

  View实现

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

  View接口中定义的方法多为showXXX()方法。

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

  @Override

  public 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得到数据。

  什么是android mvp模式,android mvp模式有什么弊端,什么是android mvp模式,android mvp模式有什么弊端,第3张

  todo-mvp-loaders

  使用Loader的优势:

  去掉了回调, 自动实现数据的异步加载;

  当内容改变时回调出新数据;

  当应用因为configuration变化而重建loader时, 自动重连到上一个loader.

  Diff with todo-mvp

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

  git difftool -d todo-mvp

  添加了两个类:

  TaskLoader和TasksLoader.

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

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

  数据获取

  添加的两个新类是TaskLoader和TasksLoader, 都继承于AsyncTaskLoader, 只不过数据的类型一个是单数, 一个是复数。

  AsyncTaskLoader是基于ModernAsyncTask, 类似于AsyncTask,

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

  TasksDataSource接口的这两个方法:

  List《Task》 getTasks();

  Task getTask(@NonNull String taskId);

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

  Presenter中保存了Loader和LoaderManager, 在start()方法里initLoader, 然后onCreateLoader返回构造传入的那个loader.

  onLoadFinished()里面调用View的方法。 此时Presenter实现LoaderManager.LoaderCallbacks.

  数据改变监听

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

  private List《TasksRepositoryObserver》 mObservers = new ArrayList《TasksRepositoryObserver》();

  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:

  @Override

  public 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.

  什么是android mvp模式,android mvp模式有什么弊端,什么是android mvp模式,android mvp模式有什么弊端,第4张

  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);

  }

  @Override

  public 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);

  。。。

  }

  @Override

  public 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是这时候传入的:

  @Override

  public 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.

  @Override

  public 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层, 把应用分为了三层:

  什么是android mvp模式,android mvp模式有什么弊端,什么是android mvp模式,android mvp模式有什么弊端,第5张

  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《SaveTask.ResponseValue》() {

  @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: TasksRepositoryModule在mock和prod目录下各有一个。

  对于每一个feature的Presenter的注入是这样实现的:

  首先, 把Presenter的构造函数标记为@Inject, 然后在Activity中构造component并注入到字段:

  @Inject AddEditTaskPresenter mAddEditTasksPresenter;

  @Override

  protected 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:

  @Module

  public 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.

  */

  @Inject

  void setupListeners() {

  mAddTaskView.setPresenter(this);

  }

  todo-mvp-contentproviders

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

  什么是android mvp模式,android mvp模式有什么弊端,什么是android mvp模式,android mvp模式有什么弊端,第6张

  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.

  去掉了TaskLoader和TasksLoader. (回归到了基本的todo-mvp)。

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

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

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

  // 返回特定fiter下或全部的数据

  public Loader《Cursor》 createFilteredTasksLoader(TaskFilter taskFilter)

  // 返回特定id的数据

  public Loader《Cursor》 createTaskLoader(String taskId)

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

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

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

  @Override

  public void onTasksLoaded(List《Task》 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为例:

  @Override

  public void onLoadFinished(Loader《Cursor》 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 *** 作, 比如:

  @Test

  public 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《List《Task》》 getTasks();

  Observable《Task》 getTask(@NonNull String taskId);

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

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

  @Override

  public Observable《List《Task》》 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中:

  @Override

  public 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《Task》() {

  @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. 这样方便测试。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/dianzi/2717940.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-08-17
下一篇 2022-08-17

发表评论

登录后才能评论

评论列表(0条)

保存