在上一篇文章中讲解了为什么要组件化、组件化的概念、创建组件化框架;这篇文章则来详细讲一些关于Android组件化开发的案例,其中融合数10个项目模块......
目录介绍
-
1.实际开发案例
- 1.1 组件化实践的开源项目
- 1.1 如何创建模块
- 1.2 如何建立依赖
- 1.3 如何统一配置文件
- 1.4 组件化的基础库
- 1.5 组件模式和集成模式如何切换
- 1.6 组件化解决重复依赖
- 1.7 组件化注意要点
- 1.8 组件化时资源名冲突
- 1.9 组件化开发遇到问题
-
2.组件间通信
- 2.1 选择那个开源路由库
- 2.2 阿里Arouter基础原理
- 2.3 使用Arouter注意事项
0.组件化开发案例开源地址
github.com/yangchong21…
1.实际开发案例
1.1 组件化实践的开源项目
- 关于组件化开发一点感想
- 关于网上有许多关于组件化的博客,讲解了什么是组件化,为何要组件化,以及组件化的好处。大多数文章提供了组件化的思路,给我着手组件化开发提供了大量的便利。感谢前辈大神的分享!虽然有一些收获,但是很少有文章能够给出一个整体且有效的方案,或者一个具体的Demo。
- 但是毕竟看博客也是为了实践做准备,当着手将之前的开源案例改版成组件化案例时,出现了大量的问题,也解决了一些问题。主要是学些了组件化开发流程。
- 大多数公司慢慢着手组件化开发,在小公司,有的人由于之前没有做过组件化开发,尝试组件化也是挺好的;在大公司,有的人一去只是负责某个模块,可能刚开始组件化已经有人弄好了,那学习实践组件化那更快一些。业余实践,改版之前开源项目,写了这篇博客,耗费我不少时间,要是对你有些帮助,那我就很开心呢。由于我也是个小人物,所以写的不好,勿喷,欢迎提出建议!
- 关于组件化开源项目
- 项目整体架构模式采用:组件化+MVP+Rx+Retrofit+design+Dagger2+VLayout+X5
- 包含的模块:wanAndroid【kotlin】+干货集中营+知乎日报+番茄Todo+精选新闻+豆瓣音乐电影小说+小说读书+简易记事本+搞笑视频+经典游戏+其他更多等等
- 此项目属于业余时间练手的项目,接口数据来源均来自网络,如果存在侵权情况,请第一时间告知。本项目仅做学习交流使用,API数据内容所有权归原作公司所有,请勿用于其他用途。
- 关于开源组件化的项目地址:github.com/yangchong21…
1.1 如何创建模块
- 根据上一篇文章3.3 架构设计图可以知道
- 主工程:
- 除了一些全局配置和主 Activity 之外,不包含任何业务代码。有的也叫做空壳app,主要是依赖业务组件进行运行。
- 业务组件:
- 最上层的业务,每个组件表示一条完整的业务线,彼此之间互相独立。原则上来说:各个业务组件之间不能有直接依赖!所有的业务组件均需要可以做到独立运行的效果。对于测试的时候,需要依赖多个业务组件的功能进行集成测试的时候。可以使用app壳进行多组件依赖管理运行。
- 该案例中分为:干活集中营,玩Android,知乎日报,微信新闻,头条新闻,搞笑视频,百度音乐,我的记事本,豆瓣音乐读书电影,游戏组件等等。
- 功能组件:
- 该案例中分为,分享组件,评论反馈组件,支付组件,画廊组件等等。同时注意,可能会涉及多个业务组件对某个功能组件进行依赖!
- 基础组件:
- 支撑上层业务组件运行的基础业务服务。此部分组件为上层业务组件提供基本的功能支持。
- 该案例中:在基础组件库中主要有,网络请求,图片加载,通信机制,工具类,分享功能,支付功能等等。当然,我把一些公共第三方库放到了这个基础组件中!
1.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"] }
1.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"]}", //这里省略一部分代码 ] }
-
第二步,然后在项目中的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" //省略部分代码 }
-
第三步,在其他model中添加依赖
- implementation project(':library')即可。
1.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 中声明依赖关系。但是具体怎么用到代码中后期有待实现!
- 如何简化不熟悉组件化的人快速适应组件独立运行
- 设置多个组件开关,需要切换那个组件就改那个。如果设置一个开关,要么把所有组件切成集成模式,要么把所有组件切成组件模式,有点容易出问题。更多可以往下看!
- 严格限制公共基础组件的增长
- 随着开发不断进行,要注意不要往基础公共组件加入太多内容。而是应该减小体积!倘若是基础组件过于庞大,那么运行组件也是比较缓慢的!
1.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' }
-
集成模式如下所示
- 首先需要在yc.gradle文件中设置 isApplication=false。Sync下后,发现该组件是library
ext { isAndroidApplication = false //false:作为Lib组件存在, true:作为application存在
-
组件模式如下所示
- 首先需要在yc.gradle文件中设置 isApplication=true。Sync下后,发现该组件是application,即可针对模块进行运行
ext { 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']
}
}
}
- 具体在项目中如下所示
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'//根据包名排除 }
1.7 组件化注意要点
- 业务组件之间联动导致耦合严重
- 比如,实际开发中,购物车和首页商品分别是两个组件。但是遇到产品需求,比如过节做个活动,发个购物券之类的需求,由于购物车和商品详情页都有活动,因此会造成组件经常会发生联动。倘若前期准备不足,随着时间的推移,各个业务线的代码边界会像组件化之前的主工程一样逐渐劣化,耦合会越来越严重。
- 第一种解决方式:使用 sourceSets 的方式将不同的业务代码放到不同的文件夹,但是 sourceSets 的问题在于,它并不能限制各个 sourceSet 之间互相引用,所以这种方式并不太友好!
- 第二种解决方式:抽取需求为工具类,通过不同组件传值而达到调用关系,这样只需要改工具类即可改需求。但是这种只是符合需求一样,但是用在不同模块的场景。
- 组件化开发之数据库分离
- 比如,我现在开发的视频模块想要给别人用,由于缓存之类需要用到数据库,难道还要把这个lib还得依赖一个体积较大的第三方数据库?但是使用系统原生sql数据库又不太方便,怎么办?暂时我也没找到办法……
1.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里, 所以不要以为这是属性的限定词!
1.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");
-
2.组件间通信
2.1 选择那个开源路由库
- 比较有代表性的组件化开源框架有得到得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。
- 得到DDComponentForAndroid:一套完整有效的android组件化方案,支持组件的组件完全隔离、单独调试、集成调试、组件交互、UI跳转、动态加载卸载等功能。
- 阿里Arouter:对页面、服务提供路由功能的中间件,简单且够用好用,网上的使用介绍博客也很多,在该组件化案例中,我就是使用这个。
- Router:一款单品、组件化、插件化全支持的路由框架
2.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
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; }
-
2.3 使用Arouter注意事项
- 使用阿里路由抽取工具类,方便后期维护!
-
首先看一下网络上有一种写法。
//首先通过注解添加下面代码 @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);
-