Google官方MVP示例代码阅读笔记

DawnaJln 3年前
   <h2>写在前面</h2>    <p>这个项目很久之前就从 <strong> android-architecture </strong> 这个仓库clone了这个MVP架构的todoapp,源码也读过,不过没有整理过。最近整理资料准备毕设了,再读一遍源码,感受和以前又不同了。</p>    <p>如果各位对MVP模式不是很熟悉,可以看我之前的一篇文:</p>    <p>Android之MVP初尝试,简单易懂。下文的view一般是指MVP中的view。</p>    <h2>剥丝抽茧,理清项目结构</h2>    <p>国际惯例,上项目结构图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/47ae6458bb8884d69e66ce6564361eb1.png"></p>    <p>结构</p>    <p>从包名上很容易分辨出功能:addedittask是添加任务,data是数据管理,statistics是统计,taskdetail是任务详情,tasks是任务浏览之类的。事实上这个项目的关键也就是: <strong>Tasks</strong> 、 <strong>TaskDetail</strong> 、 <strong>AddEditTask</strong> 、 <strong>Statistics</strong> 。</p>    <p>这四个关键的地方都有相同之处:</p>    <ul>     <li>定义了view和presenter的契约</li>     <li>Activity负责fragment和presenter的创建</li>     <li>Fragment实现了view接口</li>     <li>presenter实现了presenter接口</li>    </ul>    <p>也就是说,几个功能每一个都是MVP的模式,只不过Model层是公用的。而且这个项目里View层都是Fragment,果然google推荐用Fragment自己的项目里也给我们做个示范……其实关于到底是不是要用Fragment,还是有些争议的, <a href="/misc/goto?guid=4959737928897091924" rel="nofollow,noindex">我为什么不主张使用Fragment</a> ,这篇文关于Fragment讲的比较到位了。那么到底要不要用呢?我觉得对于个体而言,不管你喜不喜欢,都要用一用,试一试,因为人要成长,必须踩坑。对于正式项目而言,则需要综合考量,使用Fragment的利是否大于弊。</p>    <p>扯远了,接下来看一下他代码仓库给的一张结构图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/35e592551c9e1ca993097a28fede2ebc.png"></p>    <p>结构图</p>    <p>可以看出来左边是数据管理,典型的Model层。而右边呢,你可能认为Activity是Presenter,事实上并不是,Presenter在Activity内,Fragment是View无疑。到这,我觉得关于这个项目结构的简介已经足够了,接下来看代码。</p>    <p>我觉得看一个Android项目的正确姿势应该是先把玩一下app,看一下功能。贴几张app的图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/83ec8d3bd9761d6b8ac1f217d5e6e03d.png"></p>    <p>首页</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ce3a2165a96b483b1448c799595df1d5.png"></p>    <p>添加任务</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4555b5ffd691f5ae2a90bc9dcd2ee3cb.png"></p>    <p>统计</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1b8bd9323cc14c805e4d60f08fbc0ff7.png"></p>    <p>任务详情</p>    <p>接着就该上入口的Activity看一下了,这个项目的入口Activity是TasksActivity,所在的包是tasks,看一下有哪些东西:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3d5bd090ec22313270cabefcd56d5a8f.png"></p>    <p>tasks</p>    <p>第一个是自定义View,第二个就是入口Activity了,第三个即上面所说的“契约”,里面包含了View接口和Presenter接口。TasksFilterType则是一个枚举,里面有三个过滤类型:所有,进行中的,完成的。TasksFragment就是MVP中的View了,TasksPresenter则是MVP中的Presenter了。看一下TasksActivity中的初始化代码:</p>    <pre>  <code class="language-java">protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.tasks_act);          Log.e(getClass().getSimpleName(),"onCreate");            // Set up the toolbar.          Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);          setSupportActionBar(toolbar);          ActionBar ab = getSupportActionBar();          ab.setHomeAsUpIndicator(R.drawable.ic_menu);          ab.setDisplayHomeAsUpEnabled(true);            /**           * 以下的DrawerLayout暂时不看了           */          // Set up the navigation drawer.          mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);          mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);          NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);          if (navigationView != null) {              setupDrawerContent(navigationView);          }            // 获取fragment并将之添加到视图上          // 悬浮按钮在这个taksFragment里设置的点击事件          TasksFragment tasksFragment =                  (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);  //        getSupportFragmentManager().findFragmentById()          if (tasksFragment == null) {              // Create the fragment              tasksFragment = TasksFragment.newInstance();              // 提供方法帮助activity加载ui              // 这个方法其实就是拿到一个事务,然后把这个fragment add到对应的id上了              ActivityUtils.addFragmentToActivity(                      getSupportFragmentManager(), tasksFragment, R.id.contentFrame);          }            // Create the presenter          mTasksPresenter = new TasksPresenter(                  Injection.provideTasksRepository(getApplicationContext()), tasksFragment);            // Load previously saved state, if available.          if (savedInstanceState != null) {              TasksFilterType currentFiltering =                      (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);              mTasksPresenter.setFiltering(currentFiltering);          }      }</code></pre>    <p>首先是初始化toolbar和侧滑,这里不必深入细节,可以跳过这俩。之后初始化fragment和presenter,初始化Fragment先是尝试通过id寻找可能已经存在的Fragment对象,如果没有,则重新创建一个Fragment对象。下一步则是创建一个presenter,最后则是让应用在横竖屏状态切换的情况下恢复数据。</p>    <p>接下来看一下View和Presenter的“契约”:</p>    <pre>  <code class="language-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 showActiveFilterLabel();            void showCompletedFilterLabel();            void showAllFilterLabel();            void showNoActiveTasks();            void showNoCompletedTasks();            void showSuccessfullySavedMessage();            boolean isActive();            void showFilteringPopUpMenu();      }        interface Presenter extends BasePresenter {            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();      }  }</code></pre>    <p>这个接口里包含了View和Presenter,可以看到View和Presenter里的方法比较多,事实上这是应该的。因为在MVP架构里,View只负责根据Presenter的指示绘制UI,View将所有的用户交互交给Presenter处理。所以Presenter的很多方法可能就是对用户的输入的处理,而有输入必然有输出,View接口定义的各个方法便是给Presenter回调的。Presenter通过回调函数将对用户的输入的处理结果推到View中,View再根据这个结果对UI进行相应的更新。而在此项目中,Fragment就是View,在Fragment的各个点击事件中都调用了Presenter的对应方法,将业务逻辑交给Presenter处理。这看起来比传统的MVC强上很多,因为传统MVC中Activity既可以认为是Controller亦可以认为是View,职责难以分离,写到后面可能一个Activity就有上千行的代码,这会为后续的维护带来不少麻烦。而MVP则将业务逻辑抽取到了Presenter中,作为View的Fragment或者Activity职责更加单一,无疑为后续的开发维护带来了便利。</p>    <p>接下来详细的看Presenter的初始化,Presenter的创建是在TasksActivity中完成的,查看其构造函数:</p>    <pre>  <code class="language-java">public TasksPresenter(@NonNull TasksRepository tasksRepository, @NonNull TasksContract.View tasksView) {          mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");          mTasksView = checkNotNull(tasksView, "tasksView cannot be null!");            mTasksView.setPresenter(this);      }</code></pre>    <p>前两个检查传入的参数是否为空,接着将其赋值给TasksPresenter内的引用,调用view的setPresenter方法,将自身传入,这样view中就可以使用presenter对象了,比直接从activity中拿看起来要优雅了不少。Presenter具体的逻辑就不看了,都是一些比较简单的代码,回顾一下打开这个app所发生的事件的流程:创建TasksActivity -> 初始化Toolbar -> 初始化侧滑 -> 创建TasksFragment对象 -> 创建TaskPresenter对象 -> 给Fragment设置Presenter对象 -> 初始化Fragment布局,这样一套流程下来,整个流程就理清了,接下来只是等待用户的输入了。</p>    <p>接下来要看的是从本文开始到现在都一直忽略了的Model:TasksRepository。不过在分析TasksRepository之前,安利一下这个项目里的实体类,写的比较优雅,我们平时写实体类时最好也能按照他的套路来写。我为什么说他写的比较优雅呢?因为各个属性或者是带返回值的方法都打上了@Nullable或者@NoNull注解来说明是否可以为空,事实上空指针这个错可以算是平时经常遇到的错了……不过如果你有良好的设计和编码习惯,是可以避免的,带上这两个注解可以在编译期给你相关的提示。不仅如此,这个实体类还复写了equals()、hashCode()和toString()方法,而且实现的方式也符合规范,关于如何复写这三个方法,在《effective java》上有很好的总结,各位可以去读一下。</p>    <pre>  <code class="language-java">/*   * Copyright 2016, The Android Open Source Project   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License.   * You may obtain a copy of the License at   *   *      http://www.apache.org/licenses/LICENSE-2.0   *   * Unless required by applicable law or agreed to in writing, software   * distributed under the License is distributed on an "AS IS" BASIS,   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   * See the License for the specific language governing permissions and   * limitations under the License.   */    package com.example.android.architecture.blueprints.todoapp.data;    import android.support.annotation.NonNull;  import android.support.annotation.Nullable;    import com.google.common.base.Objects;  import com.google.common.base.Strings;    import java.util.UUID;    /**   * Immutable model class for a Task.   */  public final class Task {        @NonNull      private final String mId;        @Nullable      private final String mTitle;        @Nullable      private final String mDescription;        private final boolean mCompleted;        /**       * Use this constructor to create a new active Task.       *       * @param title       title of the task       * @param description description of the task       */      public Task(@Nullable String title, @Nullable String description) {          this(title, description, UUID.randomUUID().toString(), false);      }        /**       * Use this constructor to create an active Task if the Task already has an id (copy of another       * Task).       *       * @param title       title of the task       * @param description description of the task       * @param id          id of the task       */      public Task(@Nullable String title, @Nullable String description, @NonNull String id) {          this(title, description, id, false);      }        /**       * Use this constructor to create a new completed Task.       *       * @param title       title of the task       * @param description description of the task       * @param completed   true if the task is completed, false if it's active       */      public Task(@Nullable String title, @Nullable String description, boolean completed) {          this(title, description, UUID.randomUUID().toString(), completed);      }        /**       * Use this constructor to specify a completed Task if the Task already has an id (copy of       * another Task).       *       * @param title       title of the task       * @param description description of the task       * @param id          id of the task       * @param completed   true if the task is completed, false if it's active       */      public Task(@Nullable String title, @Nullable String description,                  @NonNull String id, boolean completed) {          mId = id;          mTitle = title;          mDescription = description;          mCompleted = completed;      }        @NonNull      public String getId() {          return mId;      }        @Nullable      public String getTitle() {          return mTitle;      }        @Nullable      public String getTitleForList() {          if (!Strings.isNullOrEmpty(mTitle)) {              return mTitle;          } else {              return mDescription;          }      }        @Nullable      public String getDescription() {          return mDescription;      }        public boolean isCompleted() {          return mCompleted;      }        public boolean isActive() {          return !mCompleted;      }        public boolean isEmpty() {          return Strings.isNullOrEmpty(mTitle) &&                 Strings.isNullOrEmpty(mDescription);      }        @Override      public boolean equals(Object o) {          if (this == o) return true;          if (o == null || getClass() != o.getClass()) return false;          Task task = (Task) o;          return Objects.equal(mId, task.mId) &&                 Objects.equal(mTitle, task.mTitle) &&                 Objects.equal(mDescription, task.mDescription);      }        @Override      public int hashCode() {          return Objects.hashCode(mId, mTitle, mDescription);      }        @Override      public String toString() {          return "Task with title " + mTitle;      }  }</code></pre>    <p>先看一下TasksRepository所在的包的结构:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/db716bc796c063eddbca98a75bd35f55.png"></p>    <p>data</p>    <p>可以从包名上看出local是从本地读取数据,remote是远程读取,当然了,这里只是模拟远程读取。本地采用了数据库存取的方式。在TasksRepository(下文简称TR)内部有两个TasksDataSource的引用:</p>    <pre>  <code class="language-java">private final TasksDataSource mTasksRemoteDataSource;        private final TasksDataSource mTasksLocalDataSource;</code></pre>    <p>TasksDataSource是data包内的一个接口,使用接口引用,无非是想解耦,就算以后需求变更,不想采用数据库的方式存储数据,只要实现了这个接口,TR内部的代码也无需变更。TR用了单例,实现方式并不是线程安全的:</p>    <pre>  <code class="language-java">/**       * Returns the single instance of this class, creating it if necessary.       *       * @param tasksRemoteDataSource the backend data source       * @param tasksLocalDataSource  the device storage data source       * @return the {@link TasksRepository} instance       */      public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,                                                TasksDataSource tasksLocalDataSource) {          if (INSTANCE == null) {              INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);          }          return INSTANCE;      }</code></pre>    <p>说到底,他根本没有线程安全的必要,至少在这个app里,没有并发创建这个对象的场景,所以够用就行了。在TR内部使用了一个LinkedHashMap作为容器来保存Tasks,主要看一下两个方法,首先是存储:</p>    <pre>  <code class="language-java">public void saveTask(@NonNull Task task) {          checkNotNull(task);          mTasksRemoteDataSource.saveTask(task);          mTasksLocalDataSource.saveTask(task);            // Do in memory cache update to keep the app UI up to date          if (mCachedTasks == null) {              mCachedTasks = new LinkedHashMap<>();          }          mCachedTasks.put(task.getId(), task);      }</code></pre>    <p>会将传入的task存储到远程数据源和本地数据源(本地数据库)中,然后将这个task传到mCachedTasks(LinkedHashMap)中。代码比较简单,不做更多的分析,接下来看一下读取Task:</p>    <pre>  <code class="language-java">public void getTasks(@NonNull final LoadTasksCallback callback) {          checkNotNull(callback);            // Respond immediately with cache if available and not dirty          if (mCachedTasks != null && !mCacheIsDirty) {              callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));              return;          }            if (mCacheIsDirty) {              // If the cache is dirty we need to fetch new data from the network.              getTasksFromRemoteDataSource(callback);          } else {              // Query the local storage if available. If not, query the network.              mTasksLocalDataSource.getTasks(new LoadTasksCallback() {                  @Override                  public void onTasksLoaded(List<Task> tasks) {                      refreshCache(tasks);                      callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));                  }                    @Override                  public void onDataNotAvailable() {                      getTasksFromRemoteDataSource(callback);                  }              });          }      }</code></pre>    <p>这个taskId是需要获取Task的id,也是唯一标识,GetTaskCallback则是负责传递数据的接口回调。首先是从内存中读取数据,getTaskWithId方法就是,看一下代码:</p>    <pre>  <code class="language-java">private Task getTaskWithId(@NonNull String id) {          checkNotNull(id);          if (mCachedTasks == null || mCachedTasks.isEmpty()) {              return null;          } else {              return mCachedTasks.get(id);          }      }</code></pre>    <p>就从保存task的LinkedHashMap中读取数据。如果这个过程读取不到数据那么接着从本地数据源中读取数据,如果本地数据源也没有拿到这个数据,那么最终就从远程数据源中读取数据。</p>    <p>至此,我们简单的过了一遍这个项目。</p>    <h2>总结 & 再谈MVP</h2>    <p>Google这个示例项目,架构非常的清晰,也是很标准的MVP模式,项目中解耦做的也非常好。但是相对于一个功能简单的应用来说,代码量还是比较多的。当然,因为这只是一个小例子而已,可能会让人觉得反而不如普通的MVC来开发方便,但是人无远虑必有近忧。我们做东西的时候要尽量做长远的打算,不然以后可能就会被淹没在频繁的需求变更里了。Google的这个项目有非常多值得我们学习的地方,比如我们写MVP的时候也可以用一个Contract类来将View和Presenter放入其中,方便我们管理(改代码)。</p>    <p>我们都知道MVP与MVC的主要区别是View和Model不直接交互,而是通过Presenter来完成交互,这样可以修改View而不影响Model,实现了Model和View真正的完全分离。而MVP中将业务逻辑抽取放到Presenter中,使各个模块的职责更加清晰,层次明了。而且还有很关键的一点,使用MVP架构使得应用能更加方便的进行单元测试。Android中虽然有很多测试框架,但是讲实话,你不研究个一段时间很难使用那些框架进行有效的测试。而且很多测试是难以进行的,因为有的需要依赖Android环境或者UI环境。而如果使用了MVP架构,View层因为是用接口定义的,所以完全可以自己建一个View模拟视图对象,这样就可以使得我们的测试不必依赖UI环境。这样最大的好处就是我们不必花费太多的时间去研究那些测试框架,也能写出有效的单元测试,保证我们代码的质量。</p>    <p>相较于MVP的优点,其缺点也是非常明显的,从Google的这个示例代码也能看出来,代码量比较大,小型Android应用的开发用这个反而麻烦。Presenter既负责业务逻辑,又负责Model和View的交互,到后期也难免会膨胀、臃肿,最终造成这玩意可能维护起来也不简单。</p>    <p>虽然MVP还是有不足的地方,但是相较于MVC,还是更容易的写出易维护、测试的代码的,所以各位不妨都阅读一下Google的这个代码~</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/ea967db22c2d</p>    <p> </p>