本文中我们就按照口袋助理安卓项目本身,依据项目结构的演变过程,对模块化、组件化等概念进行详细的探讨
首先,最开始还是先鸡汤一下
这个开发模型应该是最简单的开发模型,而且基本上应该没有任何一个线上应用在继续使用。
所谓简单开发模型其实就是最原始的开发方式,工程中不对代码进行任何意义上的功能划分,大量的业务逻辑混杂的充斥在一个个独立界面上,譬如网络请求、数据库操作等
整个项目没有造轮子只说,也没有模块化的业务区分,项目的组成单位可以理解为单个Class
关于该模型我们就不过多的讨论了,基本只存在于个人测试或者学习的demo中
在简单开发模型的使用过程中,相对分散在各个Activity\Fragment中的业务逻辑必然出现了重复拷贝的部分,业务逻辑分层模型就应运而出。
把握“分层”这个词所表达的含义,它意味着:
业务逻辑分层模,从命名上我们也可以看出,该模型主要是对业务层进行了分层,那么什么是业务层呢?
其实在“简单开发模型”中,我们就可以根据不同模块对具体业务的依赖程度对项目进行分层,这是一种逻辑意义上的宏观分层
Basic Component Layer 基础组件层
Middleware Layer 中间件层
Business Layer 业务层
ps: 原则上来讲,Business Layer不应直接访问Basic Component Layer的开源库,而应该通过中间件层的封装接口进行调用,从而防止业务与第三方开源组件的强依赖
业务逻辑分层模型就是针对Business Layer进行了更详尽的分层,项目中会通过封装一些工具类或者模板类,将一些标准化的过程进行集中管理,从而“在代码上聚合成不同的模块、在逻辑上分割成不同的层面”。
逻辑分层模型应该也是最常见的项目结构模型,口袋助理现有的框架,就是基于逻辑分层模型的拓展。图中可以看出,app下的目录要将近80+个,其中:
业务逻辑分层模型在代码放置层面已经有了明确的模块划分与聚合,并且通过逻辑上的分层呈现出较清晰的结构(界面|service|net+DAO),通常用于早期产品的快速开发,团队规模较小的情况下。
以下是对业务分层模型更详细的说明,由于本篇侧重于整体项目级别的架构解释,并不过多解释针对业务层的结构设计,无意了解者可直接略过
为了实现具体的业务,譬如一次简单的登录操作,往往都需要集合 界面、控制、数据三个方向的实现,如何对这三个方向进行代码上的布局也延伸了不同的设计模式
3级分层逻辑应该是所有开发人员接触最早的设计模式,严格意义上它并不是一种设计模式,而只是一种设计理念
主要是对非原始数据(数据库或者文本文件等存放数据的形式)的操作层,而不是指原始数据,也就是说,是对数据库的操作,而不是数据,具体为业务逻辑层或表示层提供数据服务.
主要是针对具体的问题的操作,也可以理解成对数据层的操作,对数据业务逻辑处理,如果说数据层是积木,那逻辑层就是对这些积木的搭建。
为用户展示相关操作结果
其中数据访问层往往可以进一步细分,用一句话概括就是“Service层统领Net层和db层,对数据进行操作”
MVC全名是Model View Controller,如图,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,来控制V层和M层通信以此来达到分离视图显示和业务逻辑层。
Android的默认实现方式本身就契合MVC模式:
一般采用XML文件进行界面的描述,这些XML可以理解为AndroidApp的View。使用的时候可以非常方便的引入。同时便于后期界面的修改。逻辑中与界面对应的id不变化则代码不用修改,大大增强了代码的可维护性。
Android的控制层的重任通常落在了众多的Activity 或 Fragment的肩上。这句话也就暗含了不要在Activity中直接写耗时代码,要通过Activity交割Model业务逻辑层处理,这样做的另外一个原因是Android中的Actiivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。
我们针对业务模型,建立的数据结构和相关的操作类,就可以理解为AndroidApp的Model,Model是与View无关,而与业务相关的(感谢@Xander的讲解)。对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算等操作也是必须放在的该层的。就是应用程序中二进制的数据。
在android开发过程中,不得不面临的一个问题就是,随着页面复杂程度的提升,即便我们做了模块划分和接口隔离,Activity或者fragment中的代码量也还是过大。试想一下,一个2000+行的activity又不带注释的代码,后期维护人员的第一反应相比就是让前人狗带。
Activity内容过多的原因其实很好解释,因为Activity不仅承担了控制逻辑的任务,还承载了view的工作,譬如与用户之间的操作交互,界面的展示等,从而促生了MVP模式。
MVP从更早的MVC框架演变过来,与MVC有一定的相似性:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示
View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)
Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)
Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑。
View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试
过多关于MVP模式的讨论我们不在本篇文章中进行,有兴趣的可了解Android App的设计架构:MVC,MVP,MVVM与架构经验谈,其中也有关于MVVM的限定
组件化应该是最近几年炒得概念,如果说逻辑分层模型只是在逻辑上对整个应用进行了功能切分的话,那么组件模型就是 “从代码上来看,全局纵向实现了功能切分,业务横向实现了业务切分”,其中前者通过module的相互依赖关系实现了显式意义上的分层,而后者则通过Business Layer的各个支持独立打包的module实现
我们知道“业务逻辑分层模型”虽然在逻辑上实现了分层,但是实际上在源码放置位置结构上还是比较混乱的,全部被堆砌在app目录下。
而且,随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,由于各个组件的开放性,也导致了复杂混乱的依赖关系,它们直接相互调用、引用、高度耦合在一起。。此时我们面临着几个问题:
构建耗时,对工程所做的任何修改都必须要编译整个工程。
增加人力成本
降低开发的并发效率
业务线全量耦合,不支持灵活集成业务线,不支持分支产品
不支持业务级的插件化方案
借助组件化方案,我们在进行了组件化分离之后,各个业务线分离,逻辑变得清晰,每个业务线都可以成为另外一个业务线的上游或者下游。更重要的是,它们每一个都可以单独编译,缩减了编译的时间。也正因为这一点,各个业务线的研发也可以做到互相不干扰,加快了开发的速度。
最终,组件化(组件解耦)带来的好处,基本可以总结为以下五点:
业界对模块化 和 组件化的定义往往混淆,本文并不做具体的辩证,作为概念理解即可
组件化和模块化是很类似的一对概念,原则上说在一些场景上进行同义替换是没有问题的,两者都是对代码结构的“由大到小”的调整,两者的目的都是为了重用和解耦.
如果一定要进行区分组件化和模块化的话
模块化
组件化
插件化则不同,插件化是在开发时就将整个app拆分成很多apk模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包
严格意义上,该模型并不是组件化模型,而应该作为模块化模型,但是对源代码的放置位置进行了要求
在这里,我们讲解该模型,主要是为了说明项目的演变过程,该模型起到的过渡作用
借助组件化这一思想,我们在”单工程”模型的基础上
需要注意这是理想状态下的结构图。实际项目中,业务组件之间会产生通信,也会产生依赖(譬如订阅号中的外勤报表Acitivty继承自外勤模块的某Activity),关于这一点,我们在下文会谈
不论是jar还是aar,本质上都是Library,他们不能脱离主工程而单独的运行,因此在开发过程中每个成员的开发设备中必须同时具备主工程和各自负责的组件。
该模型提供了的源码级别业务分离,使得每个成员可以专注自己所负责的业务,并不影响其他业务,同时借助稳定的基础组件,可以极大减少代码缺陷,因而整个团队可以以并行开发的方式高效的推进开发进度.
不但如此,组件化可以灵活的让我们进行产品组装,要做的无非就是根据需求配置相应的组件,最后生产出我们想要的产品.这有点像玩积木,通过不同摆放,我们就能得到自己想要的形状.
到这里我们基本解决了“协同开发下的代码分离”、“轮子统一存放”、“只关注自己的轮子”
相对于“主工程多组件开发模型”的改进只有一点:Debug下业务组件单独打包,不依赖主工程;Releas下和主工程多组件开发模型一样
在主App多Lib开发模型下,我们完成了分割逻辑,但是:
每次修改依赖包,就需要重新编译生成lib或者aar,而后连同主工程一起编译,这也势必导致时间的浪费,对于一个大工程而言,主工程的编译可能都需要10分钟甚至更多。
而且,一些业务Lib不能独立运行和测试,必须依赖于主App,这也带来了很多的麻烦。
如何解决这些问题? 这就是我们要说的主App多子App开发模型
不难发现,该种开发模型在结构上和”主App多Lib”并无不同,唯一的区别在于:
在该种模型下,当发现某个业务组件存在缺陷,会如何做呢?比如是业务组件2出现问题,由于在Debug模式下,业务组件2作为app可以独立运行的,因此可以很容易地对该模块进行单独修改,调试.最后修改完后只需要重新编译一次整个项目即可.
不难发现该种开发模型有效的减少了全编译的次数,减少编译耗时的同时,方便开发者进行开发调试.
对测试同学来说,功能测试可以提前,并且能够及时的参与到开发环节中,将风险降到最低.
组件的切分过程是整个组件过程中最困难繁琐的部分,建议整个团队协同解决
我们在模型中虽然给出了基本的分层结构和module布局,但是在对原有代码进行切分的时候并没进行详细的分析
目前市面上虽然很多组件化的方案介绍,上到美团微信QQ这样的超级大厂,下到一个简易版的demo,但是我们也要有这样一个认知:不管大厂小厂的方案,都是可以借鉴不可以照搬的,而且对粒度的掌控在实际上也根本不存在一个完美的值。
每个公司、每个项目的情况都不一样, 大厂的方案真的适合自己么?不见得,譬如微信在微信Android模块化架构重构实践中提及的V3.x构架方案,将所有模块都设计为“.api化接口”模式,对于一些业务线没有那么复杂的项目,可能维护接口模式的成本比整个开发成本都要大。
而且鉴于口袋助理已有的项目本身,我们尽可能的先实现“粗粒度”的切分方式,后期随着对业务的理解进行再次细分,建议思路:
具体的一些原有module承担作用如下:
原Jni Module
原Common Module
原app Module
在“主App多子App开发模型”中我们讲到,对于子App而言:在Debug模式下做为单独的Application运行,在Release模式下作为Library运行,那么
我们都知道采用Gradle构建的工程中,用apply plugin: 'com.android.application’来标识该为Application,而apply plugin: 'com.android.library’标志位Library.
因此,我们可以在编译的是同通过判断构建环境中的参数来修改子工程的工作方式,在子工程的gradle脚本头部加入以下脚本片段:
if (isDebug.toBoolean()) {// gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
其中isDebug为gradle常量,定义在各个module中的gradle.properties中,顺便一提,当你修改了 properties 文件中的值时,必须要重新 sync 一下
isDebug也可以直接定义在根目录的gradle.properties中,实现统一管理,因为在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来,譬如AndroidModuleDemo
同样,有时还需要在宿主层的壳App工程中配置,实现不同的引用方式:
if (!isDebug.toBoolean()) {
compile project(':xx1')
compile project(':xx2')
} else {
compile project(':xx3')
}
需要注意的是:
更甚至的,由于release和debug模式的相关打包签名等等,都不一样,我们完全可以写两个gradle配置文件:
if (isDebug.toBoolean()) {// gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换
apply from: "host.gradle"
} else {
apply from: "library.gradle"
}
除此之外,子app中在不同的运行方式下
我们为其创建不同的目录,分别提供自己AndroidManifest.xml文件:
下来同样需要在该子工程的gradle构建脚本中根据构建方式制定:
android {
sourceSets {
main {
if(isDebug.toBoolean()) {
manifest.srcFile 'src/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/release/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
}
}
exclude ‘debug/**’
release独立开发模式下
debug集成模式下
下面是一份标准的集成开发模式下业务组件的 AndroidManifest.xml
如果确实要区分,业务模块在 debug 状态和 release 状态有不同的行为,让业务代码感知当前是否是独立打包模式,可以通过扩展 BuildConfig 这个类,在代码中通过 boolean 值来执行不同的逻辑。只需要在 gradle 中加入
if (isDebug.toBoolean()) {
buildConfigField 'boolean', 'ISAPP', 'true'
} else {
buildConfigField 'boolean', 'ISAPP', 'false'
}
当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的。
但是我们在组件化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类,那么:
ModuleB在它的ModuleBApplication中定义的方法 verifyLoginStatus():
我们尽量不要在业务层的ModuleApplication中创建功能函数,一些放在application中的方法,基本可以使用单例方式解决,如果必须使用非单例模式,大致的解决方案:
在集成调试阶段, 每个组件根本不知道自己的宿主是谁,那么当然也就不能通过访问代码的方式直接调用宿主的方法, 从而在宿主的生命周期里加入自己的逻辑代码
如果直接将每个模块的初始化代码直接复制进宿主的生命周期里, 这样未免过于暴力, 不仅代码耦合不易扩展, 而且代码还极易冲突, 所以修改宿主源码的方式也不可行
所以有没有什么方法可以让每个组件在集成调试阶段都可以独自管理自己的生命周期呢?其实解决思路很简单, 无非就是在开发时让每个组件可以独立管理自己的生命周期, 在运行时又可以让每个组件的生命周期与宿主的生命周期进行合并 (在不修改或增加宿主代码的情况下完成)
现有的解决方案大概有三种:
不要想当然的说在BaseApplication中持有一个List,其他module主动setListener就可以解决,因为这种观察者模式和常规的方式是不同的,不能简单地使用listener的方式,,常规的观察者模式,往往是观察者主动向被观察者注册监听建立联系,然而 Application一定是最开始初始化的,如果等到观察者注册后再执行相关监听器生命周期,就已经晚了。所以通过一种方式,在被观察者调用相关生命周期前,主动去找到所有的潜在观察者,并持有对应的监听器
注意一点,不能简单的通过将一些变量设置为静态变量来实现一些调用,因为 Application变量和static变量的 区别是 “在系统不够内存情况下会自动回收静态内存,这样就会引起访问全局静态错误”
使用gradle.properties
使用 AnnotationProcessor
使用 Javassist
选择第一种方法虽然增加了几步操作, 但是简单明了, 便与理解和维护, 后续人员加入也可以很快上手, 不受 Gradle 版本的影响, 也不会增加编译时间
因为各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 A Module的 A 页面跳转到 B Module的 B 页面, 或者 在A Module 中调用 B Module的某个函数,光靠模块自身是不能实现的, 所以这时必须依靠外界的其他媒介提供这个跨组件通信的服务
跨组件通信主要有以下两种场景::
我们可以选择的通信方式有:
最终我们采用的是自定义路由框架,详见:(4.2.40)阿里开源路由框架ARouter的源码分析
类似于操作系统的总线式结构,所有挂载到组件总线上的业务组件,都可以实现双向通信.而通信协议和HTTP通信协议类似,即基于URL的方式进行
由于路由框架是一种分散式的编程方式,在管理的时候是比较麻烦的,因此对于那些需要暴露给外部的跳转操作,建议在 中间件层中建立一个IntentManager,内部使用路由框架跳转到上层依赖的页面,上层不同的module统一使用Intentmanager进行跨组件的访问
当然也有 类似arouter.json的备忘录方式,但是维护麻烦而且不能保证代码规范,不建议使用
在这里,我们主要讲下 借助 “多态持有”所实现的“接口下沉”,譬如 “Module A”中,需要调用“Module B”中的BService中的方法 getListDataNet(),那么我们可以:
因为主要工作其实是把 相关函数提炼为一种接口能力,并放置到底层的中间件层中,因此叫做“接口下沉”。
具体的“多态持有”方法,建议使用ARouter
需要值得注意的是:Android架构建设之组件化、模块化建设提出了一种基于ContentProvider 的交互方式,值得思考
有同学可能绕不过来弯,既然我可以用反射的方法直接访问跨module的Service方法,那么为什么要接口下沉?
这是由于 如果不使用 BThirdServiceInterface,你用反射能拿到的只是一个 Class targetclass = Class.forName(“com.test.yhf.B.BThirdService”), 如果你想要里边的函数,你必须再次使用函数反射调用,然而函数的改变是最常见的,不依赖引用很容易改出来bug,,,
借助“接口下沉”,BThirdServiceInterface targetclass = Class.forName("com.test.yhf.workattd.BThirdService),我们就可以很容易的使用targetclass.getListDataNet()
这个就好说了,跨Module的自定义类调用,譬如POJO等实体类,工作汇报业务中使用了jxc业务中的 CrmOrderVo, 但是这个vo如果定义在业务层中,那么工作汇报就无法访问到,这就需要把这些跨组件的Pojo下沉到中间件层
不单单是Pojo类,可能一些公共的Helper、Convert方法也可以整个类的下沉
需要注意的是,虽然我们在代码上进行了组件化封装,但是实际上他们是一个完整应用,广播和eventBus都是全局共享的
例如A业务组件中有消息列表,而用户在B组件中操作某个事件后会产生一条新消息,需要通知A组件刷新消息列表,就可以用广播或eventBus来实现
EventBus 因为其解耦的特性, 如果被滥用的话会使项目调用层次结构混乱, 不便于维护和调试, 建议大家了解下 AndroidEventBus, 其独有的 Tag 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus
阿里ARouter满足了基本的需求,大厂稳定,而且学习成本较低,建议直接复用,具体的源码分析请参看(4.2.40)阿里开源路由框架ARouter的源码分析
由于有诸多的module和子工程,如果各个子工程随意引入第三方工具包或不同版本的三方包,势必会导致软件项目的混乱
因此对各个module的Gradle文件进行统一配置管理也是十分有必要的
详细可参看 Gradle依赖的统一管理
更进一步的,由于很多组件都需要buid.gradle,里面基本很多东西都是一样的,可以在主工程新建一个文件夹并创建一个模板.gradle, 在其他module中直接引入,譬如AndroidModuleDemo
apply from: rootProject.file('script/library_work.gradle')
在Android的实际开发中,一般会有这样的需求,debug和release版本不同,接口地址不同,同时控制日志是否打印等,系统为我们提供了一个很方便的类BuildConfig可以自动判断是否是debug模式
有了BuildConfig.DEBUG之后,你在代码中可以直接写入
if (BuildConfig.DEBUG) {
Log.d(TAG, "output something");
}
但是在Android Studio中,被依赖module里BuildConfig.DEBUG的值总为false,因为Library projects目前只会生成release的包.
例如module A依赖module B和module C,在Eclipse里运行时B和C里BuildConfig.DEBUG的值会是true(导出签名apk后会自动变成false);
然而在android Studio里B和C里的BuildConfig.DEBUG值总是false,A里的正常。这样就导致if(BuildConfig.DEBUG){Log.d(…)}日志无法正常显示
具体解决方法参见 (2.2.8.9) 解决被依赖module中BuildConfig.DEBUG的值总为false问题
因为我们拆分出了很多业务组件和功能组件,并在最后一起打包处理,在合并过程中就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic_back”的图标,这时候在集成模式下打包APP就会编译出错
解决方式,总的来说可以分为两种方式:前缀限制和统一管理
我们可以混杂使用
因为分了多个 module,在合并工程的时候总会出现资源引用冲突,比如两个 module 定义了同一个资源名。
这个问题也不是新问题了,做 SDK 基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。
andorid{
defaultConfig {
resourcePrefix "moudle_prefix"
}
}
但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名
另外一种方式是,将应用使用到的所有 res资源放到一个单独module中进行统一管理,尤其是图片和xml资源
重复依赖问题其实在开发中经常会遇到,比如你 compile 了一个A,然后在这个库里面又 compile 了一个B,然后你的工程中又 compile 了一个同样的B,就依赖了两次
默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。
但是如果你使用的是 project 依赖,gradle 并不会去去重,最后打包就会出现代码中有重复的类了。
通过将子App中的 compile 改为 provided,可实现只在最终的项目中 compile 对应的代码;
根据分层模型,在层与层之间都建立 Shell层,作为统一入口,跨层的应用必须通过shell层引入
需要注意的是,shell层仅是一个逻辑概念,并不见得非要建立shell module,具备汇总功能的一个module即可
我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
exclude module: 'support-v4'//根据组件名排除
exclude group: 'android.support.v4'//根据包名排除
}
}
lib module作为aar被集成时:
组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。
尤其注意一点,在app中的R.string.xx这样标量是一个 static final静态常量,而 lib中的 R.string.xx 则是static静态量,这是由于android的打包机制决定的,目前无法改变
因此在Debug模式下开发的时候,一定记得不能把 R中的变量 作为常量使用,譬如 switch case
如果一定要用,可以借鉴《Android组件化开发框架#android.library依赖注入问题》: 利用Gradle动态复制一份R类生成新的R文件(K.java),使用的时候使用新生成的K文件即可
if (tsk.name.endsWith("Resources") && tsk.name.startsWith("process")) {
def taskName = tsk.name.replace("process", "").replace("Resources", "")
def taskR2 = task("build" + taskName + "K", dependsOn: tsk) {}
taskR2.doLast {
GeneroteK.autoGenerotaK(project)
}
tsk.doLast {
println "doLast:" + name
GeneroteK.autoGenerotaK(project)
}
}
Android组件化:在Module中使用IOC框架
插上即用,拔下不影响编译
其中:
在这里给出一个开源项目示例:https://github.com/kymjs/Modularity
Basic Component Layer 基础组件层
Business Component Layer 业务组件层
Business Module Layer 业务 Module 层
app 最终工程的目录
explorer 文件浏览器 子工程:在开发阶段是以独立的 application,在 release 时才会作为 library 引入工程
memory-box 笔记 子工程:在开发阶段是以独立的 application,在 release 时才会作为 library 引入工程
//app build.gradle
apply plugin: 'com.android.application'
android {
defaultConfig{
if (isDebug.toBoolean()) {
buildConfigField 'boolean', 'ISAPP', 'false'
} else {
buildConfigField 'boolean', 'ISAPP', 'true'
}
}
}
dependencies {
if (!isDebug.toBoolean()) {
compile project(':explorer')
compile project(':memory-box')
} else {//独立开发模式先,app仅作为一个独立module,不依赖其他 业务 module
compile project(':router')
}
}
//explorer build.gradle
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
apply from: rootDir.absolutePath + '/extra_config.gradle'
} else {
apply plugin: 'com.android.library'
}
android {
defaultConfig{
if (isDebug.toBoolean()) {
buildConfigField 'boolean', 'ISAPP', 'false'
} else {
buildConfigField 'boolean', 'ISAPP', 'true'
}
}
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
}
}
设计规则:
宿主层位于最上层, 主要作用是作为一个 App 壳, 将需要的模块组装成一个完整的 App, 这一层可以管理整个 App 的生命周期(比如 Application 的初始化和各种组件以及三方库的初始化)
App壳的 Application 必须继承自 Common组件中的 BaseApplication,因为我们必须在应用的 Application 中声明我们项目中所有使用到的业务组件,还可以在这个 Application中初始化我们工程中使用到的库文件,还可以在这里解决Android引用方法数不能超过 65535 的限制,对崩溃事件的捕获和发送也可以在这里声明,建议使用观察者模式统一管理各个 业务组件层 对Application的需求
app壳工程的 AndroidManifest.xml 是我Android应用的根表单
应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份 AndroidManifest.xml 中。另外在这份表单中还声明了整个应用程序的路由协议,用于处理组件跳转的 URL
app壳工程的 build.gradle 是比较特殊的
app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,这一点从组件化工程模型图中就能体现出来,所以app壳工程是不需要单独调试单独开发的。
打包签名
Android应用的打包签名,以及buildTypes和defaultConfig都需要在这里配置,而它的dependencies则需要根据isModule的值分别依赖不同的组件,在独立开发模式下app壳工程直接依赖 业务组件层,或者为了防止报错也可以根据实际情况依赖其他业务 module,而在最终打包模式下app壳工程必须依赖业务 module
下面是一份 app壳工程 的 build.gradle文件:
apply plugin: 'com.android.application'
static def buildTime() {
return new Date().format("yyyyMMdd");
}
android {
signingConfigs {
release {
keyAlias 'guiying712'
keyPassword 'guiying712'
storeFile file('/mykey.jks')
storePassword 'guiying712'
}
}
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.guiying.androidmodulepattern"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
multiDexEnabled false
//打包时间
resValue "string", "build_time", buildTime()
}
buildTypes {
release {
//更改AndroidManifest.xml中预先定义好占位符信息
//manifestPlaceholders = [app_icon: "@drawable/icon"]
// 不显示Log
buildConfigField "boolean", "LEO_DEBUG", "false"
//是否zip对齐
zipAlignEnabled true
// 缩减resource文件
shrinkResources true
//Proguard
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//签名
signingConfig signingConfigs.release
}
debug {
//给applicationId添加后缀“.debug”
applicationIdSuffix ".debug"
//manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
buildConfigField "boolean", "LOG_DEBUG", "true"
zipAlignEnabled false
shrinkResources false
minifyEnabled false
debuggable true
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
if (isModule.toBoolean()) {
compile project(':lib_common')
} else {
compile project(':module_main')
compile project(':module_girls')
compile project(':module_news')
}
}
业务层位于中层, 里面主要是根据业务需求和应用场景拆分过后的业务模块, 每个模块之间互不依赖, 但又可以相互交互。譬如CRM业务线、进销存业务线、电销业务线、考勤业务线、工作汇报业务线等
下面是一份普通业务组件的 build.gradle文件:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//集成开发模式下排除debug文件夹中的所有Java文件
java {
exclude 'debug/**'
}
}
}
}
//设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
//但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
//resourcePrefix "girls_"
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
compile project(':lib_common')
}
中间层的“中间”并不是垂直结构中的中间,而是指作为多个上层业务的共同桥梁和服务支撑,它
业务中间件可能是一个聚合形的modul,可以指多个module, 这些module本身就是一种业务组件,但是相较于“业务层”的业务组件,业务中间件更类似于提供一种偏向底层的业务支持,譬如权限module、流程module、建联模型module、用户管理module、商店modul、通讯modul录等
业务中间件 的划分应该遵循是否为业务层大部分模块都需要的基础业务, 以及一些需要在各个业务模块之间交互的业务, 都可以划分为 业务中间件
合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 CommonBusiness 的组件中。譬如 广告module。
Common Business Middleware是一个过渡性的module, 存放一些暂时没有独立出去的业务逻辑,应该在后期不断的重构迭代中视情况进行进一步的拆分,并最终消除
公共服务 是一个名为 Middlerware 的 Module, 主要的作用是用于 业务层 各个模块之间的交互(跨组件跳转、自定义方法和类的调用), 包含自定义 Service 接口, 、可用于跨模块传递的自定义类、 和统一的跨组件跳转RouterHub
建议在 Service Middlerware中:
服务中间件件Service Middlerware的AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了
提供一种“接口下沉”的实现,譬如 “工作汇报业务Module”中,需要调用“考勤业务Module”中的指定Service方法“获取今日全公司考勤状态”,那么我们可以:
需要注意的是,无论是使用反射、ARouter、ContentProvider方式实现多态持有,都应该 通过建立 ServiceHub式的集中管理中心,以便于日后切换实现方式
这个就好说了,跨Module的自定义类调用,譬如POJO等实体类,工作汇报业务中使用了jxc业务中的CrmOrderVo, 但是这个vo如果定义在业务层中,那么工作汇报就无法访问到,这就需要把这些跨组件的Pojo下沉到中间件层
不单单是Pojo类,可能一些公共的Helper、Convert方法也可以整个类的下沉
使用Arouter方案,每个页面的路由标示是分散在各个页面上的,如果每个需要跳转的地方都各自维护,那么代码很快就不能管理了
我们需要在中间件层创建IntentModulesManager,用于集中管理各个module暴露给外部的跳转
基础 SDK 是一个名为 CommonSDK 的 一个或者多个 Module, 其中是大量功能强大的 基础组件,不依赖于任何业务,包括了一些开源库和业务无关的自研工具库, 提供给整个架构中的所有模块
基础组件的特征如下:
基础组件的 AndroidManifest.xml 是一张空表,这张表中只有基础组件的包名;
基础组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isModule 值的;另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能组件需要的jar包和开源库。
下面是一份 基础组件的 build.gradle文件:
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
//Android Support
compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:design:$rootProject.supportLibraryVersion"
}
提供为上层服务的自定义基础构件,譬如logcore日志打印、thread models线程模型、 Jni交互协议等
基础层中的 UI 组件 是一个名为 CommonRes 的 Module, 主要放置一些业务层可以通用的与 UI 有关的资源供所有业务层模块使用, 便于重用、管理和规范已有的资源
可以放置的资源类型有:
第三方 module 主要是一些业务层可以通用的 第三方库 和 第三方 SDK (比如 ARouter, 腾讯 X5 内核), 便于重用、管理和规范已有的 SDK 依赖
为了集中管理,可以创建ThirdLibrarys Module文件夹里用来放一些第三方库的 Module (某些三方库没有提供远程依赖,或者有些时候需要修改某些三方库源码,这时就需要直接依赖 Module 里的源码),然后在 settings.gradle 中加入 ‘:ThirdLibrarys:[三方库 Module 名]’, 即可在 build.gradle 中依赖
鉴于口袋助理已有的项目本身,我们尽可能的先实现“粗粒度”的切分方式,后期随着对业务的理解进行再次细分,建议思路:
_v1 环节 2018年完成
_v2 环节 时间不确定
_v3 环节 时间不确定
在本章节中,我们讨论 module结构和口袋助理架构模型的对应关系,架构模型的不同逻辑组件落实到实际项目中,它的功能到底由那个module承载
compile project(:common)
就可以持有全部基础层功能common_lib中的module应该遵循“尽可能的独立性”,就像我们引入的jar包一样
如果必须使用一些utils则自己实现自己的utils,如果需要打印日志则通过代理注入回调
万不得已的情况下:common_lib中的module可以 provide project(:common) ,
这是一种反向依赖,我们不提倡。相当于与common module 的强绑定
对于jar的引入,如果携带res相关资源,必须新建独立module引入,不可将 res资源直接复制到app目录中;
compile project(:app)
就可以持有全部中间件层功能无非是两种做法
首先,服务中间件中定义了对外pojo、service、intent
其次,业务中间件定义了对外提供服务的业务组件
最后,baseapp的定义就为中间层汇总下级的入口module,它会汇总类似jni这样的module,并向上提供服务