【干货】组件化开发,MVP模式项目实战(更新中……)

分析某OTA平台Android客户端建立模拟架构

一、工作回顾

一年中根据新酒店业务开发不断的对架构进行优化迭代,同时也综合了“XXX客户端”现有的业务场景以及现有项目结构进行分析,最终建立了AndroidModuleDevPro架构和开发模式,项目基于MVPArms、组件化开发的基础上,用来解决因为业务的增加导致项目的体积变得庞大而难以维护,同时也增强了团队协作的灵活性,可以更快捷有效的开发、测试、迭代。

二、组件化

基于现在的“XXX户端”分析,现有业务有:机票、酒店、火车票、贵宾厅、专车、门票、管家金卡、代换登机牌、安检通道、延误险理赔、会员商城、行程管理、优秀员工、航班动态、个人中心、我的钱包、常用信息、消息中心、订单中心、特价机票、特价酒店等;

从上述功能可以看出我们的APP的业务覆盖已经非常的全面,所以这也导致我们项目的代码量的猛增,由于架构设计没有跟上业务的增长,导致了我们现有的几个问题:采用单一项目结构项目代码冗余、模块间耦合度高不易于迭代维护、开发调试运行时间漫长、人员的分配无法随机调动且代码阅读成本较高、无法单一业务模块测试导致测试流程繁琐等;

所以如果能有一种开发模式可以将每一个业务单做一个APP运行岂不是可以节省不少构建的时间,并且还可以使得项目结构更加的清晰。

三、组件拆解

基于“问题描述”我们不难看出,“客户端”每一个业务单独抽离都可以作为一个完整的APP独立开发运行,所以我们通过这个思路先对项目进行较大粒度的划分,划分后的效果如下:

客户端项目架构.png

配图说明:

  • lib_common:架构基础,提供公用的架构、工具类等,不参与任何业务处理;

  • lib_protobuf:数据实体,提供数据实体、不参与任何业务处理;

  • module_app:宿主,不参与任何业务处理;

  • module_main:主框架,提供应用的基础条件,一般只用包含、启动页、主视图框架等;

  • module_user:用户组件,用户相关信息,比如:登陆、注册、修改信息、常用信息等,同时也会向外部提供公用数据的获取;

  • module_hotel:酒店组件,酒店的查询、列表、详情、下单、订单管理等;

  • module_flight_info:航班动态,查询、列表、详情;

  • module_tickets:门票组件,查询、列表、详情、下单、订单管理;

  • module_air_ticket:机票组件,查询、列表、详情、下单、订单管理;

  • module_train_ticket:火车票组件,查询、列表、详情、下单、订单管理;

  • module_insurance:延误险理赔组件,查询、列表、详情、下单、订单管理;

  • module_bording_pass:代换登机牌,查询、列表、详情、下单、订单管理;

  • module_message:消息中心,查询、列表、详情、下单、订单管理;

  • 根据业务动态添加其它

当我们将项目拆分为上述结构之后,就可以针对每一个组件进行单独的调试、开发、迭代升级等操作,并且不会影响到其他的模块,那么在实际的开发过程中怎么去实现呢?

Android Studio 是基于Gradle构建的项目,Android常用的Module类型有Android Library、Phone&Table Module、Java Library这三个,可以进行单独运行的模式为Phone&Table Module、其余的两个模式可以作为引入关系引入到项目中,所以我们基于这个特性对项目的Module类型进行动态的切换从而达到组件化中“集成模式”&“组建模式”的切换效果,思路理清了那么就直接上代码:

每个要作为组件模块的build.gradle文件都要添加下面的代码:

//isModule.toBoolean()是我们在gradle.properties文件中声明的一个变量
if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

gradle.properties文件

# 每次更改“isModule”的值后,需要点击 "Sync Project" 按钮
# isModule是“集成开发模式”和“组件开发模式”的切换开关
isModule=false

最后打包发布的时候我们肯定是整包发布,所以作为宿主的module_app要将需要整合的组件引入进来,引用关系使用下面的代码:

module_app组件中的build.gradle文件:

//依赖设置
dependencies {
    //引入基础的开发包
    implementation project(':lib_base')
    //isModule.toBoolean()是我们在gradle.properties文件中声明
    if (!isModule.toBoolean()) {
        implementation project(':module_main')
        implementation project(':module_hotel')
        implementation project(':module_order')
        implementation project(':module_user')
        //………其它的你想引入进来的模块
    }
}

这里有一个点需要注意,在Gradle 4.X之后依赖的dependencies关键字发生了变化:

api:完全等同于compile指令,没区别,你将所有的compile改成api,完全没有错。

implementation:这个指令的特点就是,对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开。

举个例子:

比如我在一个libiary中使用implementation依赖了gson库,然后我的主项目依赖了libiary,那么,我的主项目就无法访问gson库中的方法。这样的好处是编译速度会加快,推荐使用implementation的方式去依赖,如果你需要提供给外部访问,那么就使用api依赖即可

在Google IO 相关话题的中提到了一个建议,就是依赖首先应该设置为implementation的,如果没有错,那就用implementation,如果有错,那么使用api指令,这样会使编译速度增快

所以在组件build.gradle中我们推荐下面的配置:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':lib_base')
}

解决了module类型的动态配置之后我们将会面临另一个问题,就是“组件模式”“集成模式”下的配置切换问题,为什么要说这个问题呢?原因就是“组件模式”下我们会模拟很多的数据,让当前的组件处于仿真环境下,方便我们的调试开发。但是当我们要进行“集成模式”开发的时候这些配置信息都是无用的,甚至会影响我们的集成构建,所以就要“组件模式”“集成模式”下的配置进行分离处理,具体怎么做?配置如下:

在组件的build.gradle文件中

android {
    //重新设置资源指向
    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
                //组件模式下将组件使用的java目录导入进来
                java.srcDirs 'src/main/module/debug', 'src/main/java'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成开发模式只保留原有的java目录
                java.srcDirs 'src/main/java'
            }
        }
    }
}

组件内部结构是下面这样的:

image.png

经过上面的几个步骤项目组件化的结构基本级已经确立下来了,接下来我们就可以为所欲为的添加新的组件到我们的项目中,而不必担心这个组件对别的组件的影响。

四、组件通信

组件拆解完成之项目看起来确实清爽了不少,但是噩梦也随之而来。由于我们的组件是相互独立的,所以……页面之间怎么传值?页面怎么跳转?Fragment怎么加载?模块之间怎么通信?等等一些问题随之而来。

Activity跳转的问题?

在不破坏组件化结构的前提下,我们首先想到的应该是隐式跳转,通过在AndroidMainfest.xml文件中注册过滤器

<activity android:name=".CategoryActivity" >
    <intent-filter>
        <action android:name="customer_action_here" />
    </intent-filter>
</activity>

在代码中使用下面代码进行跳转操作

//创建一个隐式的 Intent 对象:Category 类别  
Intent intent = new Intent();
intent.setAction("customer_action_here");
startActivity(intent);

看似我们好像友好的解决了这个问题,先放着我们进行下一个问题。

Fragment怎么加载?

主框架UI一般都是聚合了多个组件的内部Fragment进行展示的,这个时候又当如何使用呢?

结合Java反射机制,可以直接反射出类然后创建对象来使用,由于代码量比较大这里就不展开讲解。但是问题好像是解决了。

模块间通信?

用户信息我们在很多的页面都会用到,但是我们建立了User组件,信息被隔离到了User组件内部,如果别的组件想要使用我们的User组件内的信息怎么办?

比较简单的就是将这种数据操作封装到lib_base中这样所有的组件就都可以直接使用了,但是这样无形中增加了lib_base对业务的管理这并不是最好的实现,那么还有什么方式可以解决这个问题呢?

可以在lib_base中建立业务接口UserInfoService,同时在里面编写模板方法方法:

public interface UserInfoService  {
    boolean isLogin();

    UserInfo getUserInfo();

    String getOpenId();
}

在User组件中进行实现:

public class UserInfoServiceImpl implements UserInfoService {

    @Override
    public boolean isLogin() {
        return SpUserInfo.isLogin();
    }

    @Override
    public UserInfo getUserInfo() {
        return SpUserInfo.getUserInfo();
    }

    @Override
    public String getOpenId() {
        return SpUserInfo.getOpenId();
    }
}

然后再使用java的反射机制对实现了UserInfoService接口的类进行实例化,在调用方使用父类接口UserInfoService引用,由于接口直接引用的是一个实例化后的对象,这个时候我们是直接可以通过引用关系调用到内部方法的。从而模块间数据通讯的问题也得到了解决。

问题是解决了,那么实际开发好不好用呢?

总结一下,页面跳转都要显示的指定一个隐式跳转的意图,清单文件还要去注册对应的拦截过滤。而如果遇到了Fragment就更加麻烦,首先要去反射指定的Fragment,然后才能使用。接着就是模块间的数据通讯,也是要先建立一个顶层接口,然后要去手动的反射进行实例化,接着才能使用。

上述的操作每次使用都要进行一编,无形中增加了代码的冗余,且维护性极差,所以我们就要将这些操作全都封装到一个工具框架中,使用的时候就几代码就可以完成响应的操作。

原理我们了解了,封装的原理我们也想明白了,这里也是为了不重复的造轮子就直接使用阿里巴巴开源出来的ARouter进行解耦后的页面跳转、Fragment初始化加载、页面传值注入、组件件通信。

使用方式这里不赘述,请参见官方地址ARouter

五、ARouter遇坑

经过了上面的操作,组件化的雏形终于建立起来了,那么是不是就可以愉快的撸代码呢?

如果你认为可以的话那么我只能说,少年你还是太天真。

由于解决组件通信问题使用的是ARouter解决方案,它可以很好的帮我们生成一些中间代码,让我们可以用最少代码量实现功能,但是他有一个弊端,就是ARouter为我们生成的代码是统一的一个路径“com.alibaba.android.arouter.routes”那么这个对我们有什么影响呢?

下面分析一下这个对我们有什么影响:

酒店组件.png

用户组件.png

对比上面两个组件构建后的代码中,ARouter帮我们生成了几个java文件:

ARouter$$Group$$组名

ARouter$$Providers$$组名

ARouter$$Root$$组名

如果说我们要进行开发规范制定,比如Activity的都放在act中,Fragment都放在fgt中,Service都放在service中,然后将这些规范定义在lib_base中的一个常量ARouterPath类中进行管理,然后再使用的时候直接使用ARouterPath.XXXX进行调用,在实际调试中会出现一个效果就是如下图:

酒店组件2.png
用户组件2.png

可以看到ARouter帮我们生成了类名一模一样的两个java文件,这两个文件分别位于不同组件的同包名下,所以当我们进行构建项目的时候这两个文件是会发生冲突的,其中一个会覆盖掉另一个,导致应用调用异常,所以这个时候建立规则的时候建议采用组件名称作为组名的后缀,这样可以有效的避免文件覆盖带来的错误。

六、MVP模式介绍

全称Model - View - Presenter ,MVP 是从经典的模式 MVC 模式演变来的,它们的基本思想有相同的地方:MVC的Controller层与MVP中的Presenter层负责逻辑的处理,Model提供数据,View负责显示。

image.png

之前学的是后端开发,第一个接触的就是MVC模式,用起来很舒爽,但是做了Android开发之后使用同样的MVC模式进行嵌套会发现,一切都变的不那么的美好了,因为视图层Activity/Fragment既充当了View的视图角色,又担任了Controller的角色,导致Activity/Fragment的代码量猛增,且难以阅读和维护。

image.png

作为新的设计模式,MVP与MVC有这一个重大的区别:MVP中的View并不直接使用Model,它们之间通信是公国Presenter(MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中的View会直接从Model中读取数据而不是通过Controller。

在MVP中,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的重用。

甚至可以在Model和View都没有完成的时候就可以通过编写Mock Object(即实现了Model和View的接口,但是没有具体的内容)来测试Presenter的逻辑。

所以在MVP里,应用程序的逻辑主要在Presenter中实现,其中View是相对独立一层。因此我们可以采用Presenter First的设计模式,就是根据需求先设计和开发Presenter。在这个过程中,View可以使很简单,能够把信息显示清除即可。在后面,根据具体的页面需求再调整View的样式,而对Presenter没有任何影响。

如果UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和Presenter之间放置一个Adapter。由这个Adapter来访问Model和View,避免两者之间的关联。

在MVP模式里,View只应该有简单的Set/Get的方法,用户输入和设置页面显示的内容,除此就不应该有更多的内容,决不允许直接访问Model。这也是与MVC很大的不同之处。

优点:

1、模型与视图完全分离,我们可以修改视图而不影响模型。

2、可以更高效地使用模型,因为所有的交互都发生在一个地方,Presenter内部。

3、可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑,这个特性非常有用因为视图的变化是频繁的。(建议仅限于同一个视图的使用,跨视图会出现重写不必要的接口方法的弊端)

4、可以脱离View来进行逻辑测试;

缺点:

由于对视图的渲染放在了Presenter中,所以视图与Presenter的交互过于频繁,且紧密联系,所以一旦视图发生变更,那么Presenter也要变更。比如说,原来呈现酒店详情的视图现在要呈现酒店周边餐饮了,那么Presenter也要变更。

//TODO MVP的构建模式,结合Dagger进行处理等

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容