Android MVP 详解(下)

作者: diygreen | 来源:发表于2016-04-04 23:06 被阅读23737次

    作者:李旺成###

    时间:2016年4月3日###


    上篇

    5. 最佳实践#

    好了终于要点讲自己的东西了,有点小激动。下面这些仅表示个人观点,非一定之规,各位看官按需取用,有说的不对的,敬请谅解。关于命名规范可以参考我的另一篇文章“Android 编码规范”。老规矩先上图:

    MVPBestPractice 思维导图
    在参考了 kenjuwagatsumaMVP Architecture in Android DevelopmentSaúl MolineroA useful stack on android #1, architecture 之后,我决定采用如下的分层方案来构建这个演示Demo,如下:
    分层架构方案
    总体架构可以被分成四个部分 :
    Presentation:负责展示图形界面,并填充数据,该层囊括了 View 和 Presenter (上图所示的Model我理解为 ViewModel -- 为 View 提供数据的 Model,或称之为 VO -- View Object)。
    Domain:负责实现app的业务逻辑,该层中由普通的Java对象组成,一般包括 Usecases 和 Business Logic。
    Data:负责提供数据,这里采用了 Repository 模式,Repository 是仓库管理员,Domain 需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。Android 开发中常见的数据来源有,RestAPI、SQLite数据库、本地缓存等。
    Library:负责提供各种工具和管理第三方库,现在的开发一般离不开第三方库(当然可以自己实现,但是不要重复造轮子不是吗?),这里建议在统一的地方管理(那就是建一个单独的 module),尽量保证和 Presentation 层分开。
    AndroidStudio 中构建项目

    5.1. 关于包结构划分##

    一个项目是否好扩展,灵活性是否够高,包结构的划分方式占了很大比重。很多项目里面喜欢采用按照特性分包(就是Activity、Service等都分别放到一个包下),在模块较少、页面不多的时候这没有任何问题;但是对于模块较多,团队合作开发的项目中,这样做会很不方便。所以,我的建议是按照模块划分包结构。其实这里主要是针对 Presentation 层了,这个演示 Demo 我打算分为四个模块:登录,首页,查询天气和我的(这里仅仅是为了演示需要,具体如何划分模块还得根据具体的项目,具体情况具体分析了)。划分好包之后如下图所示:


    包结构划分

    5.2. 关于res拆分##

    功能越来越多,项目越做越大,导致资源文件越来越多,虽然通过命名可以对其有效归类(如:通过添加模块名前缀),但文件多了终究不方便。得益于 Gradle,我们也可以对 res 目录进行拆分,先来看看拆分后的效果:

    按模块拆分 res 目录
    注意:resource 目录的命名纯粹是个人的命名偏好,该目录的作用是用来存放那些不需要分模块放置的资源。
    res 目录的拆分步骤如下:
    1. 首先打开 module 的 build.gradle 文件


      res 拆分 Step1
    2. 定位到 defaultConfig {} 与 buildTypes {} 之间


      res 拆分 Step2.png
    3. 在第二步定位处编辑输入 sourceSets {} 内容,具体内容如下:
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java','.apt_generated']
            aidl.srcDirs = ['src/main/aidl','.apt_generated']
            assets.srcDirs = ['src/main/assets']
            res.srcDirs =
            [
                    'src/main/res/home',
                    'src/main/res/login',
                    'src/main/res/mine',
                    'src/main/res/weather',
                    'src/main/res/resource',
                    'src/main/res/'
    
            ]
        }
    }```
    4) 在 res 目录下按照 sourceSets 中的配置建立相应的文件夹,将原来 res 下的所有文件(夹)都移动到 resource 目录下,并在各模块中建立 layout 等文件夹,并移入相应资源,最后 Sync Project 即可。
    ##5.3. 怎么写 Model##
    这里的 Model 其实贯穿了我们项目中的三个层,Presentation、Domain 和 Data。暂且称之为 Model 吧,这也我将提供 Repository 功能的层称之为 Data Layer 的缘故(有些称这一层为 Model Layer)。
    
    **首先**,谈谈我对于 Model 是怎么理解的。应用都离不开数据,而这些数据来源有很多,如网络、SQLite、文件等等。一个应用对于数据的操作无非就是:获取数据、编辑(修改)数据、提交数据、展示数据这么几类。从分层的思想和 JavaEE 开发中积累的经验来看,我觉得 Model 中的类需要分类。从功能上来划分,可以分出这么几类:
    **VO(View Object)**:视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
    **DTO(Data Transfer Object)**:数据传输对象,这个概念来源于 JavaEE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
    **DO(Domain Object)**:领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
    **PO(Persistent Object)**:持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。
    
    **注意**:关于vo、dto、do、po可以参考这篇文章-“[领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处](http://www.cnblogs.com/qixuejia/p/4390086.html)”
    
    当然这些不一定都存在,这里只是列举一下,可以有这么多分类,当然列举的也不全。
    
    **其次**,要搞清楚 Domain 层和 Data 层分别是用来做什么的,然后才知道哪些 Model 该往 Data 层中写,哪些该往 Domain 层中写。
    Data 层负责提供数据。
    Data 层不会知道任何关于 Domain 和 Presentation 的数据。它可以用来实现和数据源(数据库,REST API或者其他源)的连接或者接口。这个层面同时也实现了整个app所需要的实体类。
    Domain 层相对于 Presentation 层完全独立,它会实现应用的业务逻辑,并提供 Usecases。
    Presentation 从 Domain 层获取到的数据,我的理解就是 VO 了,VO 应该可以直接使用。
    
    >注意:这里说的直接使用是指不需要经过各种转换,各种判断了,如 Activity 中某个控件的显示隐藏是根据 VO 中的 visibility 字段来决定,那么这个最好将 visibility 作为 int 型,而且,取值为VISIBLE/INVISIBLE/GONE,或者至少是 boolean 型的。
    
    **注意**:这里所谓的业务逻辑可能会于 Presenter 的功能概念上有点混淆。打个比方,假如 usecase 接收到的是一个 json 串,里面包含电影的列表,那么把这个 json 串转换成 json 以及包装成一个 ArrayList,这个应当是由 usecase 来完成。而假如 ArrayList 的 size 为0,即列表为空,需要显示缺省图,这个判断和控制应当是由 Presenter 完成的。(上述观点参考自:[Saúl Molinero](http://saulmm.github.io/))
    
    **最后**,就是关于 Data 层,采用的 Repository 模式,建议抽象出接口来,Domain 层需要感知数据是从哪里取出来的。
    ##5.4. 怎么写 View##
    先区分一下Android View、View、界面的区别
    **Android View**: 指的是继承自android.view.View的Android组件。
    **View**:接口和实现类,接口部分用于由 Presenter 向 View 实现类通信,可以在 Android 组件中实现它。一般最好直接使用 Activity,Fragment 或自定义 View。
    **界面**:界面是面向用户的概念。比如要在手机上进行界面间切换时,我们在代码中可以通过多种方式实现,如 Activity 到 Activity 或一个 Activity 内部的 Fragment/View 进行切换。所以这个概念基于用户的视觉,包括了所有 View 中能看到的东西。
    
    那么该怎么写 View 呢?
    
    在 MVP 中 View 是很薄的一层,里面不应该有业务逻辑,所以一般只提供一些 getter 和 setter 方法,供 Presenter 操作。关于 View,我有如下建议:
    1. 简单的页面中直接使用 Activity/Fragment 作为 View 的实现类,然后抽取相应的接口
    2. 在一些有 Tab 的页面中,可以使用 Activity + Fragment ( + ViewPager) 的方式来实现,至于 ViewPager,视具体情况而定,当然也可以直接 Activity + ViewPager 或者其他的组合方式
    3. 在一些包含很多控件的复杂页面中,那么建议将界面拆分,抽取自定义 View,也就是一个 Activity/Fragment 包含多个 View(实现多个 View 接口)
    
    ##5.5. 怎么写 Presenter##
    Presenter 是 Android MVP 实现中争论的焦点,上篇中介绍了多种“MVP 框架”,其实都是围绕着**Presenter应该怎么写**。有一篇专门介绍如何设计 Presenter 的文章([Modeling my presentation layer](http://panavtec.me/modeling-presentation-layer)),个人感觉写得不错,这里借鉴了里面不少的观点,感兴趣的童鞋可以去看看。下面进入正题。
    为什么写 Presenter 会这么纠结,我认为主要有以下几个问题:
    1. 我们将 Activity/Fragment 视为 View,那么 View 层的编写是简单了,但是这有一个问题,当手机的状态发生改变时(比如旋转手机)我们应该如何处理Presenter对象,那也就是说 Presenter 也存在生命周期,并且还要“手动维护”(别急,这是引起来的,下面会细说)
    2. Presenter 中应该没有 Android Framework 的代码,也就是不需要导 Framework 中的包,那么问题来了,页面跳转,显示对话框这些情况在 Presenter 中该如何完成
    3. 上面说 View 的时候提到复杂的页面建议通过抽取自定义 View 的方式,将页面拆分,那么这个时候要怎么建立对应的 Presenter 呢
    4. View 接口是可以有多个实现的,那我们的 Presenter 该怎么写呢
    
    好,现在我将针对上面这些问题一一给出建议。
    ###5.5.1. 关于 Presenter 生命周期的问题
    先看图(更详细讲解可以看看这篇文章[Presenter surviving orientation changes with Loaders](https://medium.com/@czyrux/presenter-surviving-orientation-changes-with-loaders-6da6d86ffbbf))
    ![Presenter生命周期](https://img.haomeiwen.com/i1233754/a9a829de0250462f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    如上图所示,方案1和方案2都不够优雅(这也是很多“MVP 框架”采用的实现方案),而且并不完善,只适用于一些场景。而方案3,让人耳目一新,看了之后不禁想说 Loader 就是为 Presenter 准备的啊。这里我们抓住几个关键点就好了:
    * Loader 是 **Android 框架**中提供的
    * Loader 在手机状态改变时是**不会被销毁**的
    * Loader 的生命周期是是由**系统控制**的,会在Activity/Fragment不再被使用后**由系统回收**
    * Loader 与 Activity/Fragment 的生命周期绑定,所以**事件会自己分发**
    * 每一个 Activity/Fragment 持有**自己的 Loader 对象**的引用
    * 具体怎么用,在 [Antonio Gutierrez](https://medium.com/@czyrux) 的文章已经阐述的很明白,我就不再赘述了
    
    > 好吧,我有一点要补充,上面说的方案1和方案2不是说就没有用了,还是视具体情况而定,如果没有那么多复杂的场景,那么用更简单的方案也未尝不可。能解决问题就好,不要拘泥于这些条条框框...(话说,咱这不是为了追求完美吗,哈哈)
    
    ###5.5.2. 关于页面跳转和显示Dialog###
    首先说说页面跳转,前一阵子忙着重构公司的项目,发现项目中很多地方使用 startActivity() 和使用 Intent 的 putExtra() 显得很乱;更重要的是从 Intent 中取数据的时候需要格外小心——类型要对应,key 要写对,不然轻则取不到数据,重则 Crash。还有一点,就是当前 Activity/Fragment 必须要知道目标 Activity 的类名,这里耦合的很严重,有没有。当时就在想这是不是应该封装一下啊,或者有更好的解决方案。于是,先在网上搜了一下,知乎上有类似的提问,有人建议写一个 Activity Router(Activity 路由表)。嗯,正好和我的思路类似,那就开干。
    
    我的思路很简单,在 util 包中定义一个 NavigationManager 类,在该类中按照模块使用注释先分好区块(为什么要分区块,去看看我的 “[Android 编码规范](http://www.jianshu.com/p/0a984f999592#)”)。然后为每个模块中的 Activity 该如何跳转,定义一个静态方法。
    
    如果不需要传递数据的,那就很简单了,只要传入调用者的 Context,直接 new 出 Intent,调用该 Context 的 startActivity() 方法即可。代码如下:
    ![导航管理类-跳转系统页面](https://img.haomeiwen.com/i1233754/1729b46c1b2709d8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    ![导航管理类-跳转不需要传递数据的页面](https://img.haomeiwen.com/i1233754/e958031db7c46841.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    如果需要传递数据呢?刚才说了,使用 Bundle 或者 putExtra() 这种方式很不优雅,而且容易出错(那好,你个给优雅的来看看,哈哈)。确实,我没想到比较优雅的方案,在这里我提供一个粗糙的方案,仅供大家参考一下,如有你有更好的,那麻烦也和我分享下。
    
    我的方案是这样的,使用序列化对象来传递数据(建议使用 Parcelable,不要偷懒去用 Serializable,这个你懂的)。为需要传递数据的 Activity 新建一个实现了 Parcelable 接口的类,将要传递的字段都定义在该类中。其他页面需要跳转到该 Activity,那么就需要提供这个对象。在目标 Activity 中获取到该对象后,那就方便了,不需要去找对应的 key 来取数据了,反正只要对象中有的,你就能直接使用。
    
    > 注意:这里我建议将序列化对象中的所有成员变量都定义为 public 的,一来,可以减少代码量,主要是为了减少方法数(虽说现在对于方法数超 64K 有比较成熟的 dex 分包方案,但是尽量不超不是更好);二来,通过对象的 public 属性直接读写比使用 getter/setter 速度要快(听说的,没有验证过)。
    
    > 注意:这里建议在全局常量类(没有,那就定义一个,下面会介绍)中定义一个唯一的 INTENT_EXTRA_KEY,往 Bundle 中存和取得时候都用它,也不用去为命名 key 费神(命名从来不简单,不是吗),取的时候也不用思考是用什么 key 存的,简单又可以避免犯错。
    
    具体如下图所示:
    ![导航管理类-跳转需要传递数据的页面](https://img.haomeiwen.com/i1233754/8e7e60b8e75c0696.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    ![导航管理类-传递数据](https://img.haomeiwen.com/i1233754/cf4de86178b55378.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    ![导航管理类-获取传递的数据](https://img.haomeiwen.com/i1233754/6cae34270d70ff30.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    导航管理类代码如下:
    ```java
    //==========逻辑方法==========
        public static <T> T getParcelableExtra(Activity activity) {
            Parcelable parcelable = activity.getIntent().getParcelableExtra(NavigateManager.PARCELABLE_EXTRA_KEY);
            activity = null;
            return (T)parcelable;
        }
    
        private static void overlay(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            setFlags(intent, flags);
            putParcelableExtra(intent, parcelable);
            context.startActivity(intent);
            context = null;
        }
    
        private static void overlay(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            putParcelableExtra(intent, parcelable);
            context.startActivity(intent);
            context = null;
        }
    
        private static void overlay(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
            Intent intent = new Intent(context, targetClazz);
            putSerializableExtra(intent, serializable);
            context.startActivity(intent);
            context = null;
        }
    
        private static void overlay(Context context, Class<? extends Activity> targetClazz) {
            Intent intent = new Intent(context, targetClazz);
            context.startActivity(intent);
            context = null;
        }
    
        private static void forward(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            setFlags(intent, flags);
            intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
            context.startActivity(intent);
            if (isActivity(context)) return;
            ((Activity)context).finish();
            context = null;
        }
    
        private static void forward(Context context, Class<? extends Activity> targetClazz, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            putParcelableExtra(intent, parcelable);
            context.startActivity(intent);
            if (isActivity(context)) return;
            ((Activity)context).finish();
            context = null;
        }
    
        private static void forward(Context context, Class<? extends Activity> targetClazz, Serializable serializable) {
            Intent intent = new Intent(context, targetClazz);
            putSerializableExtra(intent, serializable);
            context.startActivity(intent);
            if (isActivity(context)) return;
            ((Activity)context).finish();
            context = null;
        }
    
        private static void forward(Context context, Class<? extends Activity> targetClazz) {
            Intent intent = new Intent(context, targetClazz);
            context.startActivity(intent);
            if (isActivity(context)) return;
            ((Activity)context).finish();
            context = null;
        }
    
        private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags) {
            Intent intent = new Intent(context, targetClazz);
            if (isActivity(context)) return;
            ((Activity)context).startActivityForResult(intent, flags);
            context = null;
        }
    
        private static void startForResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            if (isActivity(context)) return;
            putParcelableExtra(intent, parcelable);
            ((Activity)context).startActivityForResult(intent, flags);
            context = null;
        }
    
        private static void setResult(Context context, Class<? extends Activity> targetClazz, int flags, Parcelable parcelable) {
            Intent intent = new Intent(context, targetClazz);
            setFlags(intent, flags);
            putParcelableExtra(intent, parcelable);
            if (isActivity(context)) return;
            ((Activity)context).setResult(flags, intent);
            ((Activity)context).finish();
        }
    
        private static boolean isActivity(Context context) {
            if (!(context instanceof Activity)) return true;
            return false;
        }
    
        private static void setFlags(Intent intent, int flags) {
            if (flags < 0) return;
            intent.setFlags(flags);
        }
    
        private static void putParcelableExtra(Intent intent, Parcelable parcelable) {
            if (parcelable == null) return;
            intent.putExtra(PARCELABLE_EXTRA_KEY, parcelable);
        }
    
        private static void putSerializableExtra(Intent intent, Serializable serializable) {
            if (serializable == null) return;
            intent.putExtra(PARCELABLE_EXTRA_KEY, serializable);
        }
    

    传递数据用的序列化对象,如下:

    public class DishesStockVO implements Parcelable {
    
        public boolean isShowMask; 
        public int pageNum; 
    
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeByte(isShowMask ? (byte) 1 : (byte) 0);
            dest.writeInt(this.pageNum);
        }
    
        public DishesStockVO() {
        }
    
        protected DishesStockVO(Parcel in) {
            this.isShowMask = in.readByte() != 0;
            this.pageNum = in.readInt();
        }
    
        public static final Creator<DishesStockVO> CREATOR = new Creator<DishesStockVO>() {
            public DishesStockVO createFromParcel(Parcel source) {
                return new DishesStockVO(source);
            }
    
            public DishesStockVO[] newArray(int size) {
                return new DishesStockVO[size];
            }
        };
    
        @Override
        public String toString() {
            return "DishesStockVO{" +
                    "isShowMask=" + isShowMask +
                    ", pageNum=" + pageNum +
                    '}';
        }
    }
    

    好像,还没入正题。这里再多说一句,beautifulSoup 写了一篇文章,说的就是 Android 路由表框架的,可以去看看——“Android路由框架设计与实现”。

    好了,回到主题,在 Presenter 中该如何处理页面跳转的问题。在这里我建议简单处理,在 View Interface 中定义好接口(方法),在 View 的实现类中去处理(本来就是它的责任,不是吗?)。在 View 的实现类中,使用 NavigationManager 工具类跳转,达到解耦的目的。如下图所示:


    对页面跳转的处理

    显示对话框
    我在这里采用和页面跳转的处理类似的方案,这也是 View 的责任,所以让 View 自己去完成。这里建议每个模块都定义一个相应的 XxxDialogManager 类,来管理该模块所有的弹窗,当然对于弹窗本来就不多的,那就直接在 util 包中定义一个 DialogManager 类就好了。如下图:


    对显示对话框的处理

    5.5.3. 一个页面多个View的问题###

    对于复杂页面,一般建议拆成多个自定义 View,那么这就引出一个问题,这时候是用一个 Presenter 好,还是定义多个 Presenter 好呢?我的建议是,每个 View Interface 对应一个 Presenter,如下图所示:


    一个页面多个 View 处理

    5.5.4. 一个View有两个实现类的问题###

    有些时候会遇到这样的问题,只是展示上有差别,两个页面上所有的操作都是一样的,这就意味着 View Interface 是一样的,只是有两个实现类。

    这个问题该怎么处理,或许可以继续使用同样的Presenter并在另一个Android组件中实现View接口。不过这个界面似乎有更多的功能,那要不要把这些新功能加进这个Presenter呢?这个视情况而定,有多种方案:一是将Presenter整合负责不同操作,二是写两个Presenter分别负责操作和展示,三是写一个Presenter包含所有操作(在两个View相似时)。记住没有完美的解决方案,编程的过程就是让步的过程。(参考自:Christian Panadero PaNaVTECModeling my presentation layer
    如下图所示:

    一个 View 多个实现类处理

    5.6. 关于 RestAPI##

    一般项目当中会用到很多和服务器端通信用的接口,这里建议在每个模块中都建立一个 api 包,在该包下来统一处理该模块下所有的 RestAPI。
    如下图所示:


    统一管理 RestAPI

    对于网络请求之类需要异步处理的情况,一般都需要传入一个回调接口,来获取异步处理的结果。对于这种情况,我建议参考 onClick(View v) {} 的写法。那就是为每一个请求编一个号(使用 int 值),我称之为 taskId,可以将该编号定义在各个模块的常量类中。然后在回调接口的实现类中,可以在回调方法中根据 taskId 来统一处理(一般是在这里分发下去,分别调用不同的方法)。
    如下图所示:


    定义 taskId
    异步任务回调处理

    5.6. 关于项目中的常量管理##

    Android 中不推荐使用枚举,推荐使用常量,我想说说项目当中我一般是怎么管理常量的。
    灵感来自 R.java 类,这是由项目构建工具自动生成并维护的,可以进去看看,里面是一堆的静态内部类,如下图:


    Android 中的 R 文件

    看到这,可能大家都猜到了,那就是定义一个类来管理全局的常量数据,我一般喜欢命名为 C.java。这里有一点要注意,我们的项目是按模块划分的包,所以会有一些是该模块单独使用的常量,那么这些最好不要写到全局常量类中,否则会导致 C 类膨胀,不利于管理,最好是将这些常量定义到各个模块下面。如下图所示:


    全局常量 C 类

    5.7. 关于第三方库##

    Android 开发中不可避免要导入很多第三方库,这里我想谈谈我对第三方库的一些看法。关于第三方库的推荐我就不做介绍了,很多专门说这方面的文章。

    5.7.1. 挑选第三方库的一些建议###

    1. 项目中确实需要(这不是废话吗?用不着,我要它干嘛?呵呵,建议不要为了解决一个小小的问题导入一个大而全的库)
    2. 使用的人要多(大家都在用的一般更新会比较快,出现问题解决方案也多)
    3. 效率和体量的权衡(如果效率没有太大影响的情况下,我一般建议选择体量小点的,如,Gson vs Jackson,Gson 胜出;还是 65K 的问题)

    5.7.2. 使用第三方库尽量二次封装###

    为什么要二次封装?
    为了方便更换,说得稍微专业点为了降低耦合。
    有很多原因可能需要你替换项目中的第三方库,这时候如果你是经过二次封装的,那么很简单,只需要在封装类中修改一下就可以了,完全不需要去全局检索代码。
    我就遇到过几个替换第三方库的事情:

    1. 替换项目中的统计埋点工具
    2. 替换网络框架
    3. 替换日志工具

    那该怎么封装呢?
    一般的,如果是一些第三方的工具类,都会提供一些静态方法,那么这个就简单了,直接写一个工具类,提供类似的静态方法即可(就是用静态工厂模式)。
    如下代码所示,这是对系统 Log 的简单封装:

    /**
     * Description: 企业中通用的Log管理
     * 开发阶段LOGLEVEL = 6
     * 发布阶段LOGLEVEL = -1
     */
    
    public class Logger {
    
        private static int LOGLEVEL = 6;
        private static int VERBOSE = 1;
        private static int DEBUG = 2;
        private static int INFO = 3;
        private static int WARN = 4;
        private static int ERROR = 5;
        
        public static void setDevelopMode(boolean flag) {
            if(flag) {
                LOGLEVEL = 6;
            } else {
                LOGLEVEL = -1;
            }
        }
        
        public static void v(String tag, String msg) {
            if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
                Log.v(tag, msg);
            }
        }
        
        public static void d(String tag, String msg) {
            if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
                Log.d(tag, msg);
            }
        }
        
        public static void i(String tag, String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
                Log.i(tag, msg);
            }
        }
        
        public static void w(String tag, String msg) {
            if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
                Log.w(tag, msg);
            }
        }
        
        public static void e(String tag, String msg) {
            if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
                Log.e(tag, msg);
            }
        }
        
    }
    

    现在如果想替换为 orhanobutLogger,那很简单,代码如下:

    /**
     * Description: 通用的Log管理工具类
     * 开发阶段LOGLEVEL = 6
     * 发布阶段LOGLEVEL = -1
     */
    
    public class Logger {
    
        public static String mTag = "MVPBestPractice";
        private static int LOGLEVEL = 6;
        private static int VERBOSE = 1;
        private static int DEBUG = 2;
        private static int INFO = 3;
        private static int WARN = 4;
        private static int ERROR = 5;
    
        static {
            com.orhanobut.logger.Logger
                    .init(mTag)                     // default PRETTYLOGGER or use just init()
                    .setMethodCount(3)              // default 2
                    .hideThreadInfo()               // default shown
                    .setLogLevel(LogLevel.FULL);    // default LogLevel.FULL
        }
        
        public static void setDevelopMode(boolean flag) {
            if(flag) {
                LOGLEVEL = 6;
                com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.FULL);
            } else {
                LOGLEVEL = -1;
                com.orhanobut.logger.Logger.init().setLogLevel(LogLevel.NONE);
            }
        }
        
        public static void v(@NonNull String tag, String msg) {
            if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.v(tag, msg);
                com.orhanobut.logger.Logger.t(tag).v(msg);
            }
        }
    
        public static void d(@NonNull String tag, String msg) {
            if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.d(tag, msg);
                com.orhanobut.logger.Logger.t(tag).d(msg);
            }
        }
        
        public static void i(@NonNull String tag, String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.t(tag).i(msg);
            }
        }
        
        public static void w(@NonNull String tag, String msg) {
            if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.w(tag, msg);
                com.orhanobut.logger.Logger.t(tag).w(msg);
            }
        }
        
        public static void e(@NonNull String tag, String msg) {
            if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.e(tag, msg);
                com.orhanobut.logger.Logger.t(tag).e(msg);
            }
        }
    
        public static void e(@NonNull String tag, Exception e) {
            tag = checkTag(tag);
            if(LOGLEVEL > ERROR) {
    //          Log.e(tag, e==null ? "未知错误" : e.getMessage());
                com.orhanobut.logger.Logger.t(tag).e(e == null ? "未知错误" : e.getMessage());
            }
        }
    
        public static void v(String msg) {
            if(LOGLEVEL > VERBOSE && !TextUtils.isEmpty(msg)) {
    //          Log.v(mTag, msg);
                com.orhanobut.logger.Logger.v(msg);
            }
        }
    
        public static void d(String msg) {
            if(LOGLEVEL > DEBUG && !TextUtils.isEmpty(msg)) {
    //          Log.d(mTag, msg);
                com.orhanobut.logger.Logger.d(msg);
            }
        }
    
        public static void i(String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
    //          Log.i(mTag, msg);
                com.orhanobut.logger.Logger.i(msg);
            }
        }
    
        public static void w(String msg) {
            if(LOGLEVEL > WARN && !TextUtils.isEmpty(msg)) {
    //          Log.w(mTag, msg);
                com.orhanobut.logger.Logger.v(msg);
            }
        }
    
        public static void e(String msg) {
            if(LOGLEVEL > ERROR && !TextUtils.isEmpty(msg)) {
    //          Log.e(mTag, msg);
                com.orhanobut.logger.Logger.e(msg);
            }
        }
    
        public static void e(Exception e) {
            if(LOGLEVEL > ERROR) {
    //          Log.e(mTag, e==null ? "未知错误" : e.getMessage());
                com.orhanobut.logger.Logger.e(e == null ? "未知错误" : e.getMessage());
            }
        }
    
        public static void wtf(@NonNull String tag, String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.t(tag).wtf(msg);
            }
        }
    
        public static void json(@NonNull String tag, String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.t(tag).json(msg);
            }
        }
    
        public static void xml(@NonNull String tag, String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
                tag = checkTag(tag);
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.t(tag).xml(msg);
            }
        }
    
        public static void wtf(String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.wtf(msg);
            }
        }
    
        public static void json(String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.json(msg);
            }
        }
    
        public static void xml(String msg) {
            if(LOGLEVEL > INFO && !TextUtils.isEmpty(msg)) {
    //          Log.i(tag, msg);
                com.orhanobut.logger.Logger.xml(msg);
            }
        }
    
        private static String checkTag(String tag) {
            if (TextUtils.isEmpty(tag)) {
                tag = mTag;
            }
            return tag;
        }
    
    

    这里是最简单的一些替换,如果是替换网络框架,图片加载框架之类的,可能要多费点心思去封装一下,这里可以参考“门面模式”。(在这里就不展开来讲如何对第三库进行二次封装了,以后有时间专门写个帖子)

    5.7.3. 建立单独的 Module 管理所有的第三库###

    原因前面已经说过了,而且操作也很简单。网上有不少拆分 Gradle 文件的方法,讲的都很不错。那我们就先从最简单的做起,赶快行动起来,把项目中用到的第三方库都集中到 Library Module 中来吧。

    5.8. MVP vs MVVM##

    关于 MVP 和 MVVM 我只想说一句,它们并不是相斥的。具体它们是怎么不相斥的,markzhai 的这篇文章“MVPVM in Action, 谁告诉你MVP和MVVM是互斥的”说得很详细。

    5.9. Code##

    抱歉,要食言了,AndroidStudio 出了点问题,代码还没写完,代码估计要这周末才能同步到 GitHub 上了,目前只上传了一个空框架。

    5.10. 小结##

    历时三天的 MVP 总结,总算要告一段落了。前期断断续续地花了将近一周左右零散的时间去调研 MVP,直到正式开始码字的时候才发现准备的还不够。看了很多文章,有观点一致的,也有观点很不一致的。最关键的是,自己对于 MVP 还没有比较深刻的认知,所以在各种观点中取舍花了很长时间。
    这算得上是我第一次真正意义上的写技术性的文章,说来惭愧,工作这么长时间了,现在才开始动笔。
    总体来说,写得并不尽如人意,套一句老话——革命尚未成功,同志仍需努力。这算是一次尝试,希望以后会越写越顺畅。在这里给各位坚持看到此处的看官们问好了,祝大家一同进步。(欢迎大家围观我的GitHub,周末更新,会渐渐提交更多有用的代码的)

    6. 进阶与不足##

    鉴于本人能力有限,还有很多想写的和该写的内容没有写出来,很多地方表达的也不是很清晰。下面说一说我觉得还有哪些不足和下一步要进阶的方向。

    1. 说好的“show me the code”,代码呢?(再次抱歉了)
    2. 上篇当中关于各种 Presenter 方案只是做了简单的罗列,并没有仔细分析各个方案的优点和不足
    3. 没有形成自己的框架(呵呵,好高骛远了,但是梦想还是要有的...)
    4. 没有单元测试(项目代码都还没有呢,提倡 TDD 不是,呵呵)
    5. 很多细节没有介绍清楚(如关于Model、Domain、Entity 等概念不是很清晰)
    6. 很多引用的观点没有指明出处(如有侵权,马上删除)
      ......

    最后想说一句,没有完美的架构,没有完美的框架,赶紧编码吧!

    7. 附录##

    Android MVP 总结资料汇总
    附上我的思维导图:
    MVPBestPractice.mmap
    MVP总结.mmap
    Presenter生命周期.mmap
    怎么写Presenter.mmap

    参考:
    https://segmentfault.com/a/1190000003871577
    http://www.open-open.com/lib/view/open1450008180500.html
    http://www.myexception.cn/android/2004698.html
    http://gold.xitu.io/entry/56cbf38771cfe40054eb3a34
    http://kb.cnblogs.com/page/531834/
    http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
    http://www.open-open.com/lib/view/open1446377609317.html
    http://my.oschina.net/mengshuai/blog/541314?fromerr=3J2TdbiW
    http://gold.xitu.io/entry/56fcf1f75bbb50004d872e74
    https://github.com/googlesamples/android-architecture/tree/todo-mvp-loaders/todoapp
    http://blog.zhaiyifan.cn/2016/03/16/android-new-project-from-0-p3/
    http://android.jobbole.com/82375/
    http://blog.csdn.net/weizhiai12/article/details/47904135
    http://android.jobbole.com/82051/
    http://android.jobbole.com/81153/
    http://blog.chengdazhi.com/index.php/115
    http://blog.chengdazhi.com/index.php/131
    http://www.codeceo.com/article/android-mvp-practice.html
    http://www.wtoutiao.com/p/h01nn2.html
    http://blog.jobbole.com/71209/
    http://www.cnblogs.com/tianzhijiexian/p/4393722.html
    https://github.com/xitu/gold-miner/blob/master/TODO/things-i-wish-i-knew-before-i-wrote-my-first-android-app.md
    http://gold.xitu.io/entry/56cd79c12e958a69f944984c
    http://blog.yongfengzhang.com/cn/blog/write-code-that-is-easy-to-delete-not-easy-to/
    http://kb.cnblogs.com/page/533808/

    相关文章

      网友评论

      • 白天不睡觉:作者你好,5.2.4的排版乱了,看到的话麻烦调整下哦~
      • 忘就忘了吧:厉害!介绍的很全面,代码架构也很清晰。第一次知道mvp还有这么多的变种。
      • 生椰拿铁锤:非常非常nice也很非常详细全面的MVP文章!
      • sugaryaruan:有个细节,mipmap和drawable,记得曾经看过这两者的区别:mipmap只适合放 App启动图标,工程中用到的png图片需要放在drawable文件夹下。在你工程里,统统放在mipmap里了?
      • FynnJason:5.7.3. 建立单独的 Module 管理所有的第三库

        原因前面已经说过了,而且操作也很简单。网上有不少拆分 Gradle 文件的方法,讲的都很不错。那我们就先从最简单的做起,赶快行动起来,把项目中用到的第三方库都集中到 Library Module 中来吧。

        实在没有理解这个操作,新建了module,在module中导入了gilde,然后我在主项目中调用Glide的方法,虽然能调用,但并没有效果啊,是还需要怎么操作吗?
      • 1d0e5516138e:很有想法
      • George吴逸云:很有用,感谢大神的付出,需要精读细读
      • d28bbffa6ff9:楼主写的不错,想问下楼主用的什么画图工具
      • 彩色的黑hyp:项目正在由mvc改为mvp架构,网络加载部分遇到了问题。使用volley类库加载,每次发送请求时候都要携带token验证,如果token过期加载失败,同时返回volleyError.statusCode=403,提示重新登录,跳转到登录界面。加载方法在model中实现,但是Activity跳转属于view,这部分代码不知道怎么用按照mvp来实现?求知道的大神解答。谢谢啦
      • 叨码:大神,快一年了,,demo咋还没出来?:yum:
      • ceb96b1591a1:楼主文章写得不错。不过我发现MVP真有点扯淡,浪费时间,半个小时能完成的事情偏要1个小时去做,还有,我们使用IDE找类方法的时候是可以点击过去看相关逻辑的,而MVP点击过去的时候都是接口没有逻辑,这会很让人抓狂,感觉更难维护。而那些粗暴的码农更是:“不要跟我说什么底层原理,框架内核!老夫敲代码就是一把梭!复制!粘贴!拿起键盘就是干!”。
        ceb96b1591a1: @EllforS 我mvc+mvvm混合使用,简单的就直接在activity里写,复杂的mvvm,特别是复杂表单,mvvm的双向绑定特别爽,再加上gson,解析跟提交基本上一行代码搞定。
        EllforS:额……查看接口实现不是ctrl+t么?MVP写起来麻烦,维护代码时候爽的一批,看着MVC Activity里那无边无际的代码,脑袋都疼
        8199305256ca:@墨名次 IDE是可以直接跳转到方法实现的位置,这个不应该成为“更难维护”的原因。并且接口使得java更加强大的武装了“面向对象”编程思想,现在更流行的说法是“面向接口”编程。另外,接口的定义,使得代码更加的规范,多人协同开发最离不开的就是接口定义部分。
      • JY_666:原来觉得挺简单的,又复杂化了
      • ddb15933f32d:不知道为啥github上的项目没有更新了
      • HelloVass:那啥,请问楼主的 Activity 路由管理类和 Dialog 的管理类在哪里?github地址能贴下吗?
      • d0cf4c32d2e5:学的了很多东西
      • QSJH:很华丽,但并不实用
      • 2837652731ab:大神目前源码还没更新啊。。。
      • 北方南山:你说的loader是不是就是https://github.com/googlesamples/android-architecture 里面的
        todo-mvp-loaders/ - Based on todo-mvp, fetches data using Loaders.
        呢?
      • 北方南山:已经三个月了,楼主的示例代码还没有呢
      • 62bb24c1f322:什么时候自己可以打个框架
      • 045e902c39aa:楼主什么时候上传代码,先上框架呗,单元测试后续再加上,大家都等着呢呀
      • 无聊的ddd:送上膝盖
      • 85b0b18227c2:代码还是没上传啊。。。2 month ago..
      • 接地气的二呆:楼主 github 代码什么时候能上传啊 ~~等了很久了 :no_mouth:
        diygreen: @接地气的二呆 正在准备单元测试相关的东西,这一段项目忙……
      • SkySeraph:好清晰的模式变得好乱~
      • f291f5438afa:不太明白 “5.6. 关于 RestAPI”这部分的作用,Data 包含local、remote,remote的具体实现是通过restapi与服务器交互,为什么在Presentation这里还会有个Restapi,有什么区别呢?还是我理解错了?
      • yzytmac:楼主用心了
      • Enowr:资源文件拆分后 layout 文件xmlns 报错:uri is not registered;
        这怎么解决呢
      • d6f9e5cf6524:adapter应该划分到那一层呢?
      • CK07:楼主GitHub的demo什么时候更新啊,期待交流
        diygreen: @ClownQiang 嗯,😁,搞定测试就更新
        ClownQiang:@diygreen 快快更新Github哈,文章写的很不错,期待看到代码实现~
        diygreen: @CK07 尽快吧,再完善测试相关的内容
      • MiracleWong:思维套图的背景色尽可能的是白色或者浅色吧。这个样子真心不好看。我们程序员用暗黑的Theme写代码习惯了
        diygreen:@CK07 😁,以后会注意把思维导图尽量做漂亮点
        CK07:@diygreen 我觉得很酷啊不错的
        diygreen: @MiracleWong哈哈😄,谢谢建议,以后注意
      • 双核孤城:关于没有单元测试,大神能否后续有时间加个,学习下 :smile:
      • Androidad:Android 中不推荐使用枚举,推荐使用常量
        OneBelowZero:。。。枚举在编译前就生成了 并不影响太多性能啊。。。
        diygreen: @Androidad 呵呵→_→,非我一家之言
      • c37099579567:最近看到的最好的技术文章..!!

        另外一个建议,思维导图能否有一份图片格式的,直观而且不和具体软件绑定.
        diygreen: @月夜未明gg 好建议,等下补上
      • eb09931d882a:谢谢。
      • 7385e71d6a20:用心学习!
        diygreen:@苏女 :smile:
      • 接地气的二呆:好文 收藏再细看
      • 天接水0001:加油,不错
        diygreen: @天接水0001 加油
      • alighters:Follow一下大神,最近也在用clean的架构,不过改了一些,毕竟架构是为了解决存在的问题,而一旦过于复杂,又不太符合小团队快速开发的目的,说起都是一把泪啊。
        diygreen: @lighters_wei 嗯,符合项目需要的才是最好的

      本文标题:Android MVP 详解(下)

      本文链接:https://www.haomeiwen.com/subject/lxeulttx.html