作者:杨充
链接:
https://juejin.im/post/5c46e6fb6fb9a049a5713bcc
本文由作者授权发布。
这是我见过写得相当细致的一篇文章了,而且作者的项目非常用心,文章非常长,建议点完收藏继续看。
组件化综合案例,包含微信新闻,头条视频,美女图片,百度音乐,干活集中营,玩Android,豆瓣读书电影,知乎日报等等模块。架构模式:组件化+MVP+Rx+Retrofit+Desgin+Dagger2+阿里VLayout+腾讯X5+腾讯bugly。安装阿里编码规约插件,不断修正不合理代码和最大程度去除黄色警告!
目录介绍
1.为什么要组件化
1.1 为什么要组件化
1.2 现阶段遇到的问题
2.组件化的概念
2.1 什么是组件化
2.2 区分模块化与组件化
2.3 组件化优势好处
2.4 区分组件化和插件化
2.5 application和library
3.创建组件化框架
3.1 传统APP架构图
3.2 组件化需要考虑问题
3.3 架构设计图
3.4 组件通信是通过路由转发
3.5 解决考虑问题
3.6 业务组件的生命周期
3.7 Fragment通信难点
4.实际开发案例
4.1 组件化实践的开源项目
4.1 如何创建模块
4.2 如何建立依赖
4.3 如何统一配置文件
4.4 组件化的基础库
4.5 组件模式和集成模式如何切换
4.6 组件化解决重复依赖
4.7 组件化注意要点
4.8 组件化时资源名冲突
4.9 组件化开发遇到问题
5.组件间通信
5.1 选择那个开源路由库
5.2 阿里Arouter基础原理
5.3 使用Arouter注意事项
6.关于其他
6.1 参考博客链接
6.2 我的博客介绍
6.3 开源项目地址
1.1 为什么要组件化
APP迭代维护成本增高
投资界,新芽,项目工厂等APP自身在飞速发展,版本不断迭代,新功能不断增加,业务模块数量不断增加,业务上的处理逻辑越变越复杂,同时每个模块代码也变得越来越多,这就引发一个问题,所维护的代码成本越来越高,稍微一改动可能就牵一发而动全身,改个小的功能点就需要回归整个APP测试,这就对开发和维护带来很大的挑战。
多人组合需要组件化
APP 架构方式是单一工程模式,业务规模扩大,随之带来的是团队规模扩大,那就涉及到多人协作问题,每个移动端软件开发人员势必要熟悉如此之多代码,如果不按照一定的模块组件机制去划分,将很难进行多人协作开发,随着单一项目变大,而且Andorid项目在编译代码方面就会变得非常卡顿,在单一工程代码耦合严重,每修改一处代码后都需要重新编译打包测试,导致非常耗时。
1.2 现阶段遇到的问题
结合投资界,新芽客户端分析
1. 代码量膨胀,不利于维护,不利于新功能的开发。项目工程构建速度慢,在一些电脑上写两句代码,重新编译整个项目,测试的话编译速度起码 10-20 分钟,有的甚至更长。
2. 不同模块之间代码耦合严重,有时候修改一处代码而牵动许多模块。每个模块之间都有引用第三方库,但有些第三方库版本不一致,导致打包APP时候代码冗余,容易引起版本冲突。
3. 现有项目基于以前其他人项目基础上开发,经手的人次过多,存在着不同的代码风格,项目中代码规范乱,类似的功能写法却不一样,导致不统一。
2.1 什么是组件化
什么是组件化呢?
组件(Component)是对数据和方法的简单封装,功能单一,高内聚,并且是业务能划分的最小粒度。
组件化是基于组件可重用的目的上,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,使得整个软件系统也做到电路板一样,是单个或多个组件元件组装起来,哪个组件坏了,整个系统可继续运行,而不出现崩溃或不正常现象,做到更少的耦合和更高的内聚。
2.2 区分模块化与组件化
模块化
模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等。
组件化
组件化就是更关注可复用性,更注重关注点分离,如果从集合角度来看的话,可以说往往一个模块包含了一个或多个组件,或者说模块是一个容器,由组件组装而成。
简单来说,组件化相比模块化粒度更小,两者的本质思想都是一致的,都是把大往小的方向拆分,都是为了复用和解耦,只不过模块化更加侧重于业务功能的划分,偏向于复用,组件化更加侧重于单一功能的内聚,偏向于解耦。
2.3 组件化优势好处
简单来说就是提高工作效率,解放生产力,好处如下:
1.提高编译速度,从而提高并行开发效率。
问题:那么如何提高编译速度的呢?组件化框架可以使模块单独编译调试,可以有效地减少编译的时间。
2.稳定的公共模块采用依赖库方式
提供给各个业务线使用,减少重复开发和维护工作量。代码简洁,冗余量少,维护方便,易扩展新功能。
3.每个组件有自己独立的版本,可以独立编译、测试、打包和部署。
针对开发程序员多的公司,组件化很有必要,每个人负责自己的模块,可以较少提交代码冲突。
为新业务随时集成提供了基础,所有业务可上可下,灵活多变。
各业务线研发可以互不干扰、提升协作效率,并控制产品质量。
4.避免模块之间的交叉依赖,做到低耦合、高内聚。
5.引用的第三方库代码统一管理,避免版本统一,减少引入冗余库。
这个可以创建一个公共的gradle管理的文件,比如一个项目有十几个组件,想要改下某个库或者版本号,总不至于一个个修改吧。这个时候提取公共十分有必要
6.定制项目可按需加载,组件之间可以灵活组建,快速生成不同类型的定制产品。
2.4 区分组件化和插件化
组件化和插件化的区别
组件化不是插件化,插件化是在【运行时】,而组件化是在【编译时】。换句话说,插件化是基于多APK的,而组件化本质上还是只有一个 APK。
组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。
组件化的目标
组件化的目标之一就是降低整体工程(app)与组件的依赖关系,缺少任何一个组件都是可以存在并正常运行的。app主工程具有和组件进行绑定和解绑的功能。
2.5 application和library
在studio中,对两种module进行区分,如下所示
一种是基础库library,比如常见第三方库都是lib,这些代码被其他组件直接引用。
另一种是application,也称之为Component,这种module是一个完整的功能模块。比如分享module就是一个Component。
为了方便,统一把library称之为依赖库,而把Component称之为组件,下面所讲的组件化也主要是针对Component这种类型。
在项目的build.gradle文件中
//控制组件模式和集成模式if (rootProject.ext.isDouBanApplication) { //是Component,可以独立运行 apply plugin: 'com.android.application'} else { //是lib,被依赖 apply plugin: 'com.android.library'}
if (rootProject.ext.isDouBanApplication) {
//是Component,可以独立运行
apply plugin: 'com.android.application'
} else {
//是lib,被依赖
apply plugin: 'com.android.library'
}
2.6 注意第三方sdk拆分问题
看了很多博客,几乎没有博客说出在拆分业务组件时,遇到第三方sdk集成的问题。比如:当你的app可以使用微信登陆,在app主工程时,登陆是正常的,这个时候你是通过主工程app的包名去微信开放平台申请id和key值。
但是当你将登陆注册拆分出独立的业务组件时,则该组件的包名是跟app主工程包名不一样的,那么这个时候,如果切换成组件模式则第三方登陆就有可能出现问题。
也就是说,你使用某些第三方sdk时,当初用app的包名去申请得到key值[这个值是根据包名生成的],然后当你拆分业务组件时,自然组件包名和app包名不一样,那么当切换成组件application可以独立运行时,则可能会出现bug,由包名导致的问题。
个人建议,涉及到第三方sdk拆分,可以封装成lib被依赖即可,或者你刻意把包名弄成一样的。
3组件化的概念
3.1 传统APP架构图
传统APP架构图,如图所示,从网上摘来的……
存在的问题
普遍使用的 Android APP 技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。如下图:
3.2 组件化需要考虑问题
分而治之,并行开发,一切皆组件。要实现组件化,无论采用什么样的技术方式,需要考虑以下七个方面问题:
代码解耦。
如何将一个庞大的工程分成有机的整体?这个需要一步步来了!
对已存在的项目进行模块拆分,模块分为两种类型,一种是功能组件模块,封装一些公共的方法服务等,作为依赖库对外提供;另一种是业务组件模块,专门处理业务逻辑等功能,这些业务组件模块最终负责组装APP。
组件单独运行。
因为每个组件都是高度内聚的,是一个完整的整体,如何让其单独运行和调试?
通过 Gradle脚本配置方式,进行不同环境切换,我自己操作是添加一个boolean值的开关。比如只需要把 Apply plugin: ‘com.android.library’ 切换成Apply plugin: ‘com.android.application’ 就可以独立运行呢!
需要注意:当切换到application独立运行时,需要在AndroidManifest清单文件上进行设置,因为一个单独调试需要有一个入口的Activity。
组件间通信。
由于每个组件具体实现细节都互相不了解,但每个组件都需要给其他调用方提供服务,那么主项目与组件、组件与组件之间如何通信就变成关键?
这个我是直接用阿里开源的路由框架,当然你可以根据需要选择其他大厂的开源路由库。引用阿里的ARouter框架,通过注解方式进行页面跳转。
组件生命周期。
这里的生命周期指的是组件在应用中存在的时间,组件是否可以做到按需、动态使用、因此就会涉及到组件加载、卸载等管理问题。
集成调试。
在开发阶段如何做到按需编译组件?一次调试中可能有一两个组件参与集成,这样编译时间就会大大降低,提高开发效率。
代码隔离。
组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦,如何从根本上避免组件之间的直接引用?目前做法是主项目和业务组件都会依赖公共基础组件库,业务组件通过路由服务依赖库按需进行查找,用于不同组件之间的通信。
告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为AAR包集成到“APP壳工程”中,组成一个完整功能的 APP。
3.3 架构设计图
组件化架构图
业务组件之间是独立的,互相没有关联,这些业务组件在集成模式下是一个个 Library,被 APP 壳工程所依赖,组成一个具有完整业务功能的 APP 应用,但是在组件开发模式下,业务组件又变成了一个个Application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。
3.4 组件通信是通过路由转发
传统以前工程下模块
记得刚开始进入Android开发工作时,只有一个app主工程,后期几乎所有的需求都写在这个app主工程里面。只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。
导致后期改项目为组件化的时候十分痛苦,不同模块之间的业务逻辑实在关联太多,但还是没办法,于是目录4步骤一步步实践。终极目标是,告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发。
组件化模式下如何通信
这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系。在这个开源项目中,我使用的阿里开源的路由框架。关于Arouter基础使用和代码分析,可以看我这篇博客:
Arouter使用与代码解析
https://github.com/yangchong211/YCBlogs
3.6 业务组件的生命周期
按照理想状态的来看待的话
各个业务组件之间没有任何依赖关系,这时我们可以把每个独立的业务组件看成一个可运行的app,所以业务组件的生命周期和应与独立的app保持一致。
3.7 Fragment通信难点
在网上看到很多博客说,如何拆分组件,按模块拆分,或者按照功能拆分。但很少有提到fragment在拆分组件时的疑问,这个让我很奇怪。
先来说一个业务需求,比如一个购物商城app,有4个模块,做法一般是一个activity+4个fragment,这个大家都很熟悉,这四个模块分别是:首页,发现,购物车,我的。然后这几个页面是用fragment写的,共用一个宿主activity,那么在做组件化的时候,我想把它按照业务拆分成首页,发现,购物车和我的四个独立的业务模块。
遇到疑问:
如果是拆分成四个独立的业务模块,那么对应的fragment肯定要放到对应的组件中,那么这样操作,当主工程与该业务组件解绑的情况下,如何拿到fragment和传递参数进行通信。
Fragment 中 开启Activity带requestCode,开启的Activity关闭后,不会回调Fragment中的onActivityResult。只会调用Fragment 所在Activity的onActivityResult。
多fragment单activity拦截器不管用,难道只能用于拦截activity的跳转?那如果是要实现登录拦截的话,那不是只能在PathReplaceService中进行了?
网络解决办法
第一个疑问:由于我使用阿里路由,所以我看到zhi1ong大佬说:用Router跳转到这个Activity,然后带一个参数进去,比方说tab=2,然后自己在onCreate里面自行切换。但后来尝试,还是想问问广大程序员有没有更好的办法。
第二个疑问:还是zhi1ong大佬说,通过广播,或者在Activity中转发这个事件,比方说让Fragment统一依赖一个接口,然后在Activity中转发。
4.1 组件化实践的开源项目
关于组件化开发一点感想
关于网上有许多关于组件化的博客,讲解了什么是组件化,为何要组件化,以及组件化的好处。大多数文章提供了组件化的思路,给我着手组件化开发提供了大量的便利。感谢前辈大神的分享!虽然有一些收获,但是很少有文章能够给出一个整体且有效的方案,或者一个具体的Demo。
但是毕竟看博客也是为了实践做准备,当着手将之前的开源案例改版成组件化案例时,出现了大量的问题,也解决了一些问题。主要是学些了组件化开发流程。
大多数公司慢慢着手组件化开发,在小公司,有的人由于之前没有做过组件化开发,尝试组件化也是挺好的;在大公司,有的人一去只是负责某个模块,可能刚开始组件化已经有人弄好了,那学习实践组件化那更快一些。业余实践,改版之前开源项目,写了这篇博客,耗费我不少时间,要是对你有些帮助,那我就很开心呢。由于我也是个小人物,所以写的不好,勿喷,欢迎提出建议!
关于组件化开源项目
项目整体架构模式采用:组件化+MVP+Rx+Retrofit+design+Dagger2+VLayout+X5
包含的模块:wanAndroid【kotlin】+干货集中营+知乎日报+番茄Todo+精选新闻+豆瓣音乐电影小说+小说读书+简易记事本+搞笑视频+经典游戏+其他更多等等
此项目属于业余时间练手的项目,接口数据来源均来自网络,如果存在侵权情况,请第一时间告知。本项目仅做学习交流使用,API数据内容所有权归原作公司所有,请勿用于其他用途。
关于开源组件化的项目地址:
https://github.com/yangchong211/LifeHelper
4.1 如何创建模块
根据3.3 架构设计图可以知道
主工程:
除了一些全局配置和主 Activity 之外,不包含任何业务代码。有的也叫做空壳app,主要是依赖业务组件进行运行。
业务组件:
最上层的业务,每个组件表示一条完整的业务线,彼此之间互相独立。原则上来说:各个业务组件之间不能有直接依赖!所有的业务组件均需要可以做到独立运行的效果。对于测试的时候,需要依赖多个业务组件的功能进行集成测试的时候。可以使用app壳进行多组件依赖管理运行。
该案例中分为:干活集中营,玩Android,知乎日报,微信新闻,头条新闻,搞笑视频,百度音乐,我的记事本,豆瓣音乐读书电影,游戏组件等等。
功能组件:
该案例中分为,分享组件,评论反馈组件,支付组件,画廊组件等等。同时注意,可能会涉及多个业务组件对某个功能组件进行依赖!
基础组件:
支撑上层业务组件运行的基础业务服务。此部分组件为上层业务组件提供基本的功能支持。
该案例中:在基础组件库中主要有,网络请求,图片加载,通信机制,工具类,分享功能,支付功能等等。当然,我把一些公共第三方库放到了这个基础组件中!
4.2 如何建立依赖
关于工程中组件依赖结构图如下所示
业务模块下完整配置代码
//控制组件模式和集成模式if (rootProject.ext.isGankApplication) { apply plugin: 'com.android.application'} else { apply plugin: 'com.android.library'}android { compileSdkVersion rootProject.ext.android["compileSdkVersion"] buildToolsVersion rootProject.ext.android["buildToolsVersion"] defaultConfig { minSdkVersion rootProject.ext.android["minSdkVersion"] targetSdkVersion rootProject.ext.android["targetSdkVersion"] versionCode rootProject.ext.android["versionCode"] versionName rootProject.ext.android["versionName"] if (rootProject.ext.isGankApplication){ //组件模式下设置applicationId applicationId "com.ycbjie.gank" } javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } //jdk1.8 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { main { if (rootProject.ext.isGankApplication) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } jniLibs.srcDirs = ['libs'] } }}dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':library') annotationProcessor rootProject.ext.dependencies["router-compiler"]}
if (rootProject.ext.isGankApplication) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
if (rootProject.ext.isGankApplication){
//组件模式下设置applicationId
applicationId "com.ycbjie.gank"
}
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//jdk1.8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
if (rootProject.ext.isGankApplication) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':library')
annotationProcessor rootProject.ext.dependencies["router-compiler"]
}
4.3 如何统一配置文件
由于组件化实践中模块比较多,因此配置gradle,添加依赖库时,需要考虑简化工作。那么究竟如何做呢?
第一步,首先在项目根目录下创建一个yc.gradle文件。实际开发中只需要更改该文件中版本信息即可。
我在网上看到的绝大多数案例,都是通过一个开关控件组件模式和集成模式的切换,但是这里我配置了多个组件的开关,分别控制对应的组件切换状态。
ext { isApplication = false //false:作为Lib组件存在, true:作为application存在 isAndroidApplication = false //玩Android模块开关,false:作为Lib组件存在, true:作为application存在 isLoveApplication = false //爱意表达模块开关,false:作为Lib组件存在, true:作为application存在 isVideoApplication = false //视频模块开关,false:作为Lib组件存在, true:作为application存在 isNoteApplication = false //记事本模块开关,false:作为Lib组件存在, true:作为application存在 isBookApplication = false //book模块开关,false:作为Lib组件存在, true:作为application存在 isDouBanApplication = false //豆瓣模块开关,false:作为Lib组件存在, true:作为application存在 isGankApplication = false //干货模块开关,false:作为Lib组件存在, true:作为application存在 isMusicApplication = false //音乐模块开关,false:作为Lib组件存在, true:作为application存在 isNewsApplication = false //新闻模块开关,false:作为Lib组件存在, true:作为application存在 isToDoApplication = false //todo模块开关,false:作为Lib组件存在, true:作为application存在 isZhiHuApplication = false //知乎模块开关,false:作为Lib组件存在, true:作为application存在 isOtherApplication = false //其他模块开关,false:作为Lib组件存在, true:作为application存在 android = [ compileSdkVersion : 28, buildToolsVersion : "28.0.3", minSdkVersion : 17, targetSdkVersion : 28, versionCode : 22, versionName : "1.8.2" //必须是int或者float,否则影响线上升级 ] version = [ androidSupportSdkVersion: "28.0.0", retrofitSdkVersion : "2.4.0", glideSdkVersion : "4.8.0", canarySdkVersion : "1.5.4", constraintVersion : "1.0.2" ] dependencies = [ //support "appcompat-v7" : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}", "multidex" : "com.android.support:multidex:1.0.1", //network "retrofit" : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}", "retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}", "retrofit-adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}", //这里省略一部分代码 ]}
isApplication = false //false:作为Lib组件存在, true:作为application存在
isAndroidApplication = false //玩Android模块开关,false:作为Lib组件存在, true:作为application存在
isLoveApplication = false //爱意表达模块开关,false:作为Lib组件存在, true:作为application存在
isVideoApplication = false //视频模块开关,false:作为Lib组件存在, true:作为application存在
isNoteApplication = false //记事本模块开关,false:作为Lib组件存在, true:作为application存在
isBookApplication = false //book模块开关,false:作为Lib组件存在, true:作为application存在
isDouBanApplication = false //豆瓣模块开关,false:作为Lib组件存在, true:作为application存在
isGankApplication = false //干货模块开关,false:作为Lib组件存在, true:作为application存在
isMusicApplication = false //音乐模块开关,false:作为Lib组件存在, true:作为application存在
isNewsApplication = false //新闻模块开关,false:作为Lib组件存在, true:作为application存在
isToDoApplication = false //todo模块开关,false:作为Lib组件存在, true:作为application存在
isZhiHuApplication = false //知乎模块开关,false:作为Lib组件存在, true:作为application存在
isOtherApplication = false //其他模块开关,false:作为Lib组件存在, true:作为application存在
android = [
compileSdkVersion : 28,
buildToolsVersion : "28.0.3",
minSdkVersion : 17,
targetSdkVersion : 28,
versionCode : 22,
versionName : "1.8.2" //必须是int或者float,否则影响线上升级
]
version = [
androidSupportSdkVersion: "28.0.0",
retrofitSdkVersion : "2.4.0",
glideSdkVersion : "4.8.0",
canarySdkVersion : "1.5.4",
constraintVersion : "1.0.2"
]
dependencies = [
//support
"appcompat-v7" : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
"multidex" : "com.android.support:multidex:1.0.1",
//network
"retrofit" : "com.squareup.retrofit2:retrofit:${version["retrofitSdkVersion"]}",
"retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:${version["retrofitSdkVersion"]}",
"retrofit-adapter-rxjava" : "com.squareup.retrofit2:adapter-rxjava2:${version["retrofitSdkVersion"]}",
//这里省略一部分代码
]
}
第二步,然后在项目中的lib【注意这里是放到基础组件库的build.gradle】中添加代码,如下所示
apply plugin: 'com.android.library'android { compileSdkVersion rootProject.ext.android["compileSdkVersion"] buildToolsVersion rootProject.ext.android["buildToolsVersion"] defaultConfig { minSdkVersion rootProject.ext.android["minSdkVersion"] targetSdkVersion rootProject.ext.android["targetSdkVersion"] versionCode rootProject.ext.android["versionCode"] versionName rootProject.ext.android["versionName"] }}dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api rootProject.ext.dependencies["appcompat-v7"] api rootProject.ext.dependencies["design"] api rootProject.ext.dependencies["palette"] api rootProject.ext.dependencies["glide"] api (rootProject.ext.dependencies["glide-transformations"]){ exclude module: 'glide' } annotationProcessor rootProject.ext.dependencies["glide-compiler"] api files('libs/tbs_sdk_thirdapp_v3.2.0.jar') api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" //省略部分代码}'com.android.library'
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api rootProject.ext.dependencies["appcompat-v7"]
api rootProject.ext.dependencies["design"]
api rootProject.ext.dependencies["palette"]
api rootProject.ext.dependencies["glide"]
api (rootProject.ext.dependencies["glide-transformations"]){
exclude module: 'glide'
}
annotationProcessor rootProject.ext.dependencies["glide-compiler"]
api files('libs/tbs_sdk_thirdapp_v3.2.0.jar')
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//省略部分代码
}
第三步,在其他model中添加依赖
implementation project(‘:library’)即可。
4.4 组件化的基础库
基础库组件封装
基础库组件封装库中主要包括开发常用的一些框架。可以直接看我的项目更加直观!
1、网络请求(采用Retrofit+RxJava框架),拦截器
2、图片加载(策略模式,Glide与Picasso之间可以切换)
3、通信机制(RxBus),路由ARouter简单封装工具类(不同model之间通信)
4、mvp框架,常用的base类,比如BaseActivity,BaseFragment等等
5、通用的工具类,比如切割圆角,动画工具类等等
6、自定义view(包括对话框,ToolBar布局,圆形图片等view的自定义)
7、共有的shape,drawable,layout,color等资源文件
8、全局初始化异步线程池封装库,各个组件均可以用到
组件初始化
比如,你将该案例中的新闻组件切换成独立运行的app,那么由于新闻跳转详情页需要使用到x5的WebView,因此需要对它进行初始化。最刚开始做法是,为每一个可以切换成app的组件配置一个独立的application,然后初始化一些该组件需要初始化的任务。
但是这么做,有一点不好,不是很方便管理。后来看了知乎组件化实践方案后,该方案提出,开发了一套多线程初始化框架,每个组件只要新建若干个启动 Task 类,并在 Task 中声明依赖关系。但是具体怎么用到代码中后期有待实现!
如何简化不熟悉组件化的人快速适应组件独立运行
设置多个组件开关,需要切换那个组件就改那个。如果设置一个开关,要么把所有组件切成集成模式,要么把所有组件切成组件模式,有点容易出问题。更多可以往下看!
严格限制公共基础组件的增长
随着开发不断进行,要注意不要往基础公共组件加入太多内容。而是应该减小体积!倘若是基础组件过于庞大,那么运行组件也是比较缓慢的!
4.5 组件模式和集成模式如何切换
在玩Android组件下的build.gradle文件,其他组件类似。
通过一个开关来控制这个状态的切换,module如果是一个库,会使用com.android.library插件;如果是一个应用,则使用com.android.application插件
//控制组件模式和集成模式if (rootProject.ext.isAndroidApplication) { apply plugin: 'com.android.application'} else { apply plugin: 'com.android.library'}
if (rootProject.ext.isAndroidApplication) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
集成模式如下所示
首先需要在yc.gradle文件中设置 isApplication=false。Sync下后,发现该组件是library
ext { isAndroidApplication = false //false:作为Lib组件存在, true:作为application存在
isAndroidApplication = false //false:作为Lib组件存在, true:作为application存在
组件模式如下所示
首先需要在yc.gradle文件中设置 isApplication=true。Sync下后,发现该组件是application,即可针对模块进行运行
ext { isAndroidApplication = true //false:作为Lib组件存在, true:作为application存在
isAndroidApplication = true //false:作为Lib组件存在, true:作为application存在
需要注意的地方,这个很重要
首先看看网上绝大多数的作法,非常感谢这些大神的无私奉献!但是我觉得多个组件用一个开关控制也可以,但是sourceSets里面切换成组件app时,可以直接不用下面这么麻烦,可以复用java和res文件。
接下来看看我的做法:
下面这个配置十分重要。也就是说当该玩Android组件从library切换到application时,由于可以作为独立app运行,所以序意设置applicationId,并且配置清单文件,如下所示!
在 library 和 application 之间切换,manifest文件也需要提供两套
android { defaultConfig { if (rootProject.ext.isAndroidApplication){ //组件模式下设置applicationId applicationId "com.ycbjie.android" } } sourceSets { main { if (rootProject.ext.isAndroidApplication) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } jniLibs.srcDirs = ['libs'] } }}
if (rootProject.ext.isAndroidApplication){
//组件模式下设置applicationId
applicationId "com.ycbjie.android"
}
}
sourceSets {
main {
if (rootProject.ext.isAndroidApplication) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
jniLibs.srcDirs = ['libs']
}
}
}
具体在项目中如下所示
4.6 组件化解决重复依赖
重复依赖问题说明
重复依赖问题其实在开发中经常会遇到,比如项目 implementation 了一个A,然后在这个库里面又 implementation 了一个B,然后你的工程中又 implementation 了一个同样的B,就依赖了两次。
默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。但是如果使用的是project依赖,gradle并不会去去重,最后打包就会出现代码中有重复的类了。
解决办法,举个例子
api(rootProject.ext.dependencies["logger"]) { exclude module: 'support-v4'//根据组件名排除 exclude group: 'android.support.v4'//根据包名排除 }
exclude module: 'support-v4'//根据组件名排除
exclude group: 'android.support.v4'//根据包名排除
}
4.7 组件化注意要点
业务组件之间联动导致耦合严重
比如,实际开发中,购物车和首页商品分别是两个组件。但是遇到产品需求,比如过节做个活动,发个购物券之类的需求,由于购物车和商品详情页都有活动,因此会造成组件经常会发生联动。倘若前期准备不足,随着时间的推移,各个业务线的代码边界会像组件化之前的主工程一样逐渐劣化,耦合会越来越严重。
第一种解决方式:使用 sourceSets 的方式将不同的业务代码放到不同的文件夹,但是 sourceSets 的问题在于,它并不能限制各个 sourceSet 之间互相引用,所以这种方式并不太友好!
第二种解决方式:抽取需求为工具类,通过不同组件传值而达到调用关系,这样只需要改工具类即可改需求。但是这种只是符合需求一样,但是用在不同模块的场景。
组件化开发之数据库分离
比如,我现在开发的视频模块想要给别人用,由于缓存之类需要用到数据库,难道还要把这个lib还得依赖一个体积较大的第三方数据库?但是使用系统原生sql数据库又不太方便,怎么办?暂时我也没找到办法……
4.8 组件化时资源名冲突
资源名冲突有哪些?
比如,color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。这是为何了,有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题。
尤其是如果string, color,dimens这些资源分布在了代码的各个角落,一个个去拆,非常繁琐。其实大可不必这么做。因为android在build时,会进行资源的merge和shrink。res/values下的各个文件(styles.xml需注意)最后都只会把用到的放到intermediate/res/merged/../valus.xml,无用的都会自动删除。并且最后我们可以使用lint来自动删除。所以这个地方不要耗费太多的时间。
解决办法
这个问题也不是新问题了,第三方SDK基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。
个人建议
将color,shape等放到基础库组件中,因为所有的业务组件都会依赖基础组件库。在styles.xml需注意,写属性名字的时候,一定要加上前缀限定词。假如说不加的话,有可能会在打包成aar后给其他模块使用的时候,会出现属性名名字重复的冲突,为什么呢?因为BezelImageView这个名字根本不会出现在intermediate/res/merged/../valus.xml里, 所以不要以为这是属性的限定词!
4.9 组件化开发遇到问题
如何做到各个组件化模块能获取到全局上下文
情景再现
比如,刚开始线上项目是在app主工程里创建的单利,那么在lib中或者后期划分的组件化,是无法拿到主工程的application类中的上下文。这个时候可以
解决办法
很容易,在lib里写一个Utils工具类,然后在主工程application中初始化Utils.init(this),这样就可以在lib和所有业务组件[已经依赖公共基础组件库]中拿到全局上下文呢!
butterKnife使用问题
尽管网上有不少博客说可以解决butterKnife在不同组件之间的引用。但是我在实际开发时,遇到组件模式和集成模式切换状态时,导致出现编译错误问题。要是那位在组件化中解决butterKnife引用问题,可以告诉我,非常感谢!
当组件化是lib时
不能使用switch(R.id.xx),需要使用if..else来代替。
不要乱发bus消息
如果项目中大量的使用eventbus,那么会看到一个类中有大量的onEventMainThread()方法,写起来很爽,阅读起来很痛苦。
虽然说,前期使用EventBus或者RxBus发送消息来实现组件间通信十分方便和简单,但是随着业务增大,和后期不断更新,有的还经过多个程序员前前后后修改,会使代码阅读量降低。项目中发送这个Event的地方非常多,接收这个Event的地方也很多。在后期想要改进为组件化开发,而进行代码拆分时,都不敢轻举妄动,生怕哪些事件没有被接收。
页面跳转存在问题
如果一个页面需要登陆状态才可以查看,那么会写if(isLogin()){//跳转页面}else{//跳转到登录页面},每次操作都要写这些个相同的逻辑。
原生startActivity跳转,无法监听到跳转的状态,比如跳转错误,成功,异常等问题。
有时候,后台会控制从点击按钮【不同场景下】跳转到不同的页面,假如后台配置信息错误,或者少了参数,那么跳转可能不成功或者导致崩溃,这个也没有一个好的处理机制。
阿里推出的开源框架Arouter,便可以解决页面跳转问题,可以添加拦截,或者即使后台配置参数错误,当监听到跳转异常或者跳转错误时的状态,可以直接默认跳转到首页。我在该开源案例就是这么做的!
关于跳转参数问题
先来看一下这种代码写法,这种写法本没有问题,只是在多人开发时,如果别人想要跳转到你开发模块的某个页面,那么就容易传错值。建议将key这个值,写成静态常量,放到一个专门的类中。方便自己,也方便他人。//跳转
//跳转intent.setClass(this,CommentActivity.class);intent.putExtra("id",id);intent.putExtra("allNum",allNum);intent.putExtra("shortNum",shortNum);intent.putExtra("longNum",longNum);startActivity(intent);//接收Intent intent = getIntent();int allNum = intent.getExtras().getInt("allNum");int shortNum = intent.getExtras().getInt("shortNum");int longNum = intent.getExtras().getInt("longNum");int id = intent.getExtras().getInt("id");
intent.setClass(this,CommentActivity.class);
intent.putExtra("id",id);
intent.putExtra("allNum",allNum);
intent.putExtra("shortNum",shortNum);
intent.putExtra("longNum",longNum);
startActivity(intent);
//接收
Intent intent = getIntent();
int allNum = intent.getExtras().getInt("allNum");
int shortNum = intent.getExtras().getInt("shortNum");
int longNum = intent.getExtras().getInt("longNum");
int id = intent.getExtras().getInt("id");
5.1 选择那个开源路由库
比较有代表性的组件化开源框架有得到得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。
得到DDComponentForAndroid:一套完整有效的android组件化方案,支持组件的组件完全隔离、单独调试、集成调试、组件交互、UI跳转、动态加载卸载等功能。
阿里Arouter:对页面、服务提供路由功能的中间件,简单且够用好用,网上的使用介绍博客也很多,在该组件化案例中,我就是使用这个。
Router:一款单品、组件化、插件化全支持的路由框架
5.2 阿里Arouter基础原理
这里只是说一下基础的思路
在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activityClass映射关系的类文件,然后app进程启动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址。
添加@Route注解然后编译一下,就可以生成这个类,然后看一下这个类。如下所示:
/** * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */public class ARouter$$Group$$video implements IRouteGroup { @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class, "/video/videoactivity", "video", null, -1, -2147483648)); }}
public class ARouter$$Group$$video implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/video/VideoActivity", RouteMeta.build(RouteType.ACTIVITY, VideoActivity.class, "/video/videoactivity", "video", null, -1, -2147483648));
}
}
ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。
看_ARouter类中的 _navigation方法代码,在345行。
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { final Context currentContext = null == context ? mContext : context; switch (postcard.getType()) { case ACTIVITY: // Build intent final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); // Set flags. int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // Set Actions String action = postcard.getAction(); if (!TextUtils.isEmpty(action)) { intent.setAction(action); } // Navigation in main looper. runInMainThread(new Runnable() { @Override public void run() { startActivity(requestCode, currentContext, intent, postcard, callback); } }); break; case PROVIDER: //这里省略代码 case BOARDCAST: case CONTENT_PROVIDER: case FRAGMENT: //这里省略代码 case METHOD: case SERVICE: default: return null; } return null;}
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// Set Actions
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
case PROVIDER:
//这里省略代码
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
//这里省略代码
case METHOD:
case SERVICE:
default:
return null;
}
return null;
}
5.3 使用Arouter注意事项
使用阿里路由抽取工具类,方便后期维护!
下面这种做法,是方便后期维护。
首先看一下网络上有一种写法。
//首先通过注解添加下面代码@Route(path = "/test/TestActivity")public class TestActivity extends BaseActivity {}//跳转ARouter.getInstance().inject("/test/TestActivity");
@Route(path = "/test/TestActivity")
public class TestActivity extends BaseActivity {
}
//跳转
ARouter.getInstance().inject("/test/TestActivity");
优化后的写法
下面这种做法,是方便后期维护。
//存放所有的路由路径常量public class ARouterConstant { //跳转到视频页面 public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity"; //省略部分diamagnetic}//存放所有的路由跳转,工具类public class ARouterUtils { /** * 简单的跳转页面 * @param string string目标界面对应的路径 */ public static void navigation(String string){ if (string==null){ return; } ARouter.getInstance().build(string).navigation(); }}//调用@Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)public class VideoActivity extends BaseActivity {}ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);
public class ARouterConstant {
//跳转到视频页面
public static final String ACTIVITY_VIDEO_VIDEO = "/video/VideoActivity";
//省略部分diamagnetic
}
//存放所有的路由跳转,工具类
public class ARouterUtils {
/**
* 简单的跳转页面
* @param string string目标界面对应的路径
*/
public static void navigation(String string){
if (string==null){
return;
}
ARouter.getInstance().build(string).navigation();
}
}
//调用
@Route(path = ARouterConstant.ACTIVITY_VIDEO_VIDEO)
public class VideoActivity extends BaseActivity {
}
ARouterUtils.navigation(ARouterConstant.ACTIVITY_VIDEO_VIDEO);
组件化实践项目开源地址:
https://github.com/yangchong211/LifeHelper
喜欢 就关注吧,欢迎投稿!
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/android组件化开发实践和案例分享-融合数10个项目模块/