模块化?组件化?插件化?
组件化和模块化
百度百科对组件化的解释:
解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。
组件化是一种高效的处理复杂应用系统,更好的明确功能模块作用的方式。
目的
为了解耦把复杂系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。
-
对于模块和组件,不同的人会有不同的理解,而大部门人认为是针对功能和业务不同的划分:
-
模块:针对独立的业务模块,如首页模块,聊一聊模块,信息资讯模块,社区模块等
-
组件:独立的功能性的组件,如视频播放组件,支付组件等
-
但是目的都是一样的解耦复杂的系统,明确划分各模块的职责界限。区别是一个以业务为导向,一个以功能为导向。
插件化
插件化也叫动态组件化
在Android中插件化可以理解为一项技术方案,同样使用插件化技术,将不同的组件或者模块,做成插件,动态加载,也可以满足组件化的需求,
组件化架构演进
对于早期的Eclipse项目,是没有组件化架构的概念的,只有一个Project。
Eclipse项目架构
AndroidStudio项目架构
AndroidStudio 使用了Project加多Module的方式进行项目管理,本身就是一种多模块化的开发,但是由于开发中,多模块之间产生依赖关系后 ,没有任务约束,相互的引用太多,就会变的难以维护,迭代越来越多,项目越来越复杂。
以下是一个相对较好的项目结构
-
Application层:包含业务相关的不同模块,以及一些同级的库
-
Base层:Base基类等
-
框架层:主要统一管理依赖的框架以及封装基础库:图片框架,网络框架等,
使用AndroidStudio组件化架构
以微鲤看看为原型,简单对不同业务模块进行了划分,形成了一张组件化架构图
-
APP壳:没有实际的业务只作为了一个入口,同时也防止app依赖所有的组件后,没法解耦。
-
组件层:包含项目的各个组件,组件之间相互独立,没有依赖关系,他们之间的通信调用是通过Service层的接口实现的,
-
基础层:包含通信相关的Service,Base基类,以及一些基础功能组件
组件化优缺点
优点
-
抽取公共的功能组件,多个组件甚至项目共用,提高开发效率和节省开发成本与维护成本
-
独立业务模块,可独立运行编译,多团队可以高效的并行开发与测试
-
组件之间以接口对外通信,其他组件的替换,升级不会影响其他组件,也不会受其他组件的限制
缺点
-
组件对需要的依赖度高,需求多变,可能根据业务划分的组件无法满足需求,组件做出相应的变化调整,过于复杂的业务调整会有些难度
-
单个组件的编译速度有所提高,编译整个工程app,需要耗些时间
-
组件化的实施对开发人员和团队管理人员提出了更高水平的要求,项目管理难度更大。组件间如何进行通信也是需要慎重考虑的。万事开头难,在对一个项目进行组件化分解时就好像庖丁解牛一般,你需要了解项目的“肌理筋骨”,才知道从何处下“刀”,才能更轻易的去分解项目,这就要求架构师对于项目的整体需求了如指掌
组件化开发
Android组件化开发其实就是解决如下问题的过程
-
如何根据业务需求,或现有项目,进行拆分成不同模块
-
如何隔离组件,并解决不相互依赖的组件之间的通信问题
-
解决单个组件从可以独立编译运行的APP到依赖库的转换问题
-
页面跳转问题,多个组件如何实现直接页面的跳转
实现方案
有3种
-
接口编程
-
路由框架
-
组件化框架
一般都是混合实现,没有纯路由的组件化,这种方案会导致项目对路由框架有很大的依赖性。
Demo项目结构
以微鲤看看为原型,做了个组件化Demo项目结构如下
-
App层
app
项目入口 -
组件层
-
account_component
账户组件包含登录注册,账户信息 -
app_main_comp
主组件:主页HomeActivity,(这里主要考虑有的主页功能较为复杂,所以单独分出来) -
im_component
IM组件,IM相关 -
news_component
资讯组件 -
community_component
社区相关
-
-
base层
-
base
一些base基类 -
component_service
所有组件通信的服务接口组件 -
common_library
一些三方框架,由于多个组件都会引用第三方库文件,所有用来统一管理依赖库,防止版本不统一造成一些其他问题
-
搭建流程
统一配置文件
-
因为每个组件或者库文件,都会有依赖一些好的框架,这里为了方便统一管理新建
config.build
,来全局管理build.gradle
的配置。
ext { version = [ 'compileVersion' : 28, 'targetSdkVersion': 25, 'minSdkVersion' : 19, 'versionCode' : 1, 'versionName' : "1.0.0" ] dependencies = [ 'appcompat_v7' : 'com.android.support:appcompat-v7:28.0.0', 'butterKnife' : "com.jakewharton:butterknife:$butterKnifeVersion", 'butterKnife_annotation': "com.jakewharton:butterknife- compiler:$butterKnifeVersion", 'arouter_api' : "com.alibaba:arouter-api:$arouter_api", "arouter_annotation" : "com.alibaba:arouter-compiler:$arouter_annotation" ] // 该字段标识当前是集成模式(运行APP),还是调试模式(单个组件运行编译) isRunApp = true; }
-
为每个组件创建
module.gradle
配置文件,该文件是完全复制组件下build.gradle
文件,每个组件的build.gradle
都需引入该配置文件,方便统一管理module.gradle
//引入config.gradle配置 apply from: '../config.gradle' def VERSION = project.ext.version def libs = project.ext.dependencies android { compileSdkVersion VERSION.compileVersion defaultConfig { minSdkVersion VERSION.minSdkVersion targetSdkVersion VERSION.targetSdkVersion versionCode VERSION.versionCode versionName VERSION.versionName javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] } } } } // 每个组件都是需要依赖:component_service组件进行通信的 dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) annotationProcessor libs['butterKnife_annotation'] api project(':component_service') annotationProcessor libs['arouter_annotation'] api libs['arouter_api'] }
解决独立组件编译运行问题
在组件的配置gradle文件module.gradle
中加入Boolean类型字段:isRunApp
,true
表示当前运行整个app,false
表示单个组件可以运行编译,
module.gradle
apply from: '../config.gradle' def isRunApp = project.ext.isRunApp // 这样只需要修改配置文件的,然后同步一下,就可以切换编译模式 if (isRunApp) { apply plugin: 'com.android.library' } else { apply plugin: 'com.android.application' } android {} dependencies {}
主app下的build.gradle文件也需要加入
if (isRunApp) { implementation project(':account_component') implementation project(':app_main_comp') implementation project(':news_component') implementation project(':community_component') implementation project(':im_component') }
Manifest合并问题
项目整体运行时,每个组件的manifest最终会合并,主module的manifest中会定义项目的theme
、name
、label
,单独编译时同样也是需要的,这样AndroidManifest.xml
的Application标签下有属性相同,会产生冲突。如:theme
、name
、label
等,
解决办法有2种
-
解决冲突时,需要加入
tools:replace="android:name"
表明name属性是可以背替代的,这样合并时就会自动合并,最终冲突的会编入主app的属性,如果主app不存在,会编入最后编译的module的manifest的属性 -
以主App为准,其他组件的manifest,只定义
<application></application>
,不定义任何属性,避免冲突,为独立运行的组件创建组件对应的Androidmanifest.xml
文件,并配置目录在组件的默认gradle文件中配置manifest路径
module.gradle
apply from: '../config.gradle' //... android { //... sourceSets { main { if (isRunApp) { // 不做处理,使用默认的manifest路径 } else { // 指定 组件运行时,AndroidManifest.xml的路径 manifest.srcFile 'src/main/debug/manifest/AndroidManifest.xml' } } } } dependencies {}
组件独立运行
组件独立运行也是需要Activity的入口,所有我们会为每个独立的组件创建相应入口类,入口类只是我们运行组件,调试组件的辅助类,包括一些运行调试时的一些资源文件,这些在运行时都不需要写入到项目中。
根据分析,我们在module.gradle
中配置组件运行时的资源目录和java主目录,在开发时调试时,我们就可以使用指定的目录去测试我们的功能
module.gradle
apply from: '../config.gradle' //... android { //... sourceSets { main { if (isRunApp) { // 不做处理,使用默认的manifest路径 //manifest.srcFile... resources { // 运行项目时,排除debug文件下的资源 exclude 'src/main/debug/*' } } else { //指定 组件运行时,AndroidManifest.xml的路径 manifest.srcFile 'src/main/debug/manifest/AndroidManifest.xml' // 指定资源和java文件位置 java.srcDirs = ['src/main/debug/java/','src/main/java'] res.srcDirs = ['src/main/debug/res/','src/main/res/'] } } } } dependencies {}
这样我们在debug文件下。调试我们的组件,就完全可以跑起来了。
组件初始化
组件在作为Library依赖时,是没有application生命周期的,如果某个组件需要在项目启动时,做一些初始化的操作,是无法完成的,所以在主APP的application执行oncreate时,我们要对每个组件进行初始化。
组件的初始化方式有多种,可根据项目架构来对应做初始化处理
思路:在每个需要组件中定义一个初始化组件的类,在主App的application
的onCreate()
方法中调用,由于代码隔离,主app是拿不到组件的初始化类的,这里采用反射的方式实现
-
在Comoponent_service基础层的服务组件中,定义
BaseModuleInit.java
,需要初始化的组件继承该类,做初始化的操作public abstract class BaseModuleInit { /** * 每个组件有需要首次启动初始化时使用 * @param application 主application对象 */ public abstract void applicationInit(Application application); }
-
通过
ModuleAppInits.java
记录需要初始化的类路径public class ModuleAppInits { //需要加入避免混淆的类,初始化组件时的初始化类全路径; public static final List<String> sModulesAppInits = Arrays.asList( "com.zgq.account.AccountModuleInit", "com.zgq.app_main_comp.MainModuleInit", "com.zgq.news_component.NewsModuleInit", "com.zgq.community_component.CommunityModuleInit", "com.zgq.im_component.ImModuleInit"); }
-
在Application的onCreate()方法中调用初始化
Application
/** * 初始化各组件; * 各组件没有Application,所有通过主Application的初始话,对各组件进行初始化; */ public void initModule() { for (String sModulesAppInit : ModuleAppInits.sModulesAppInits) { try { Class<?> clazz = Class.forName(sModulesAppInit); BaseModuleInit baseModuleInit = (BaseModuleInit) clazz.newInstance(); baseModuleInit.applicationInit(this); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } }
使用框架Arouter初始化
可以使用Arouter的依赖查找的功能来对组件初始化
// 1.component_service基础组件的 初始化组件的方法需要implement Arouter的Iprovider接口 public class BaseModuleInit implement Iprovider{} // 2.组件的初始化方法添加路由表 @Route(path = "/xx/xx/init") public class ComponentModuleInit extends BaseModuleInit{} // 3.在application的OnCreate()中使用路由查找该初始化类, BaseModuleInit init =(BaseModuleInit)Arouter.getInstance().build("xx/xx/init").nagigation(); init.application(this);
组件之间的通信
组件直接的交互都是通过Component_service来实现的,component_serivce为每个组件提供了数据交互的接口,每个组件只需要实现该接口,并在初始化组件的时候设置给component_service即可
举例
-
在component_service组件中为账户组件创建数据输出Service;
AccountService.java
-
账户组件实现
AccountService
,创建AccountServiceImp类对外输出数据
public interface AccountService { String getUid(); String getIsLogin(); UserInfo getUserInfo(); BaseFragment getMyFragment(BaseActivity activity); }
-
在component_service创建
serviceProvider
为各组件提供交互service,
//组件交互service输出类 public class ServiceProvider { //账户组件service private AccountService mAccountService; // 主组件service private MainComponentService mMainComponentService; // 资讯组件service private NewsService mNewsService; // 社区组件service private CommunityService mCommunityService; // IM组件service private IMService mIMService; private ServiceProvider() { } private static final ServiceProvider instance = new ServiceProvider(); public static ServiceProvider getInstance() { return instance; } // 省略 get()set()方法 }
-
在初始化组件时,使用serviceProvider.set()方法设置相应的service;
// 账户组件初始化 public class AccountModuleInit extends BaseModuleInit { @Override public void applicationInit(Application application) { ServiceProvider.getInstance().setAccountService(new AccountServiceImp()); } }
同样的serviceProvider相关的功能,也是可以通过Arouter的依赖查找来实现
页面跳转
页面的跳转统一放在component_service组件Router
类中
具体的实现可以采用路由,或者面向接口实现的传统Intent的跳转,
目前对随着项目复杂度,还有逻辑越来越多,包括push跳转,webview拦截跳转等,使用Intent的方式,我们需要写太多的if else
,后期维护成本高,所以页面的跳转建议还是路由
具体的使用参考:
Arouter:一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
这里是使用的阿里的Arouter:
public class Router { // 跳转首页 public static void startToMain(Activity mActivity) { ARouter.getInstance().build(RouterAPI.MAIN_ACT).navigation(mActivity); } } @Route(path = RouterAPI.MAIN_ACT) public class HomeActivity extends BaseActivity { public void onCreate(Bundle savedInstanceState){} }
遇到的问题
资源冲突
-
多团队协作,多组件开发时,在资源命名时,难免会有相同的时候,这时候,在合并项目的时候就会产生冲突。
解决的办法是在每个组件的
build.gradle
中配置命名约束,
// 配置资源命名约束,一般会以组件名开头 android { resourcePrefix "xxx_" }
配置命名约束后, 会限制.xml结尾的资源,包括values下的文件,如果我们不以xxx_开头的话,会报红提示的,但是一些图片资源等,还是需要手动解决冲突,多组件开发时,这就要求我们在开发过程中,形成好的命名习惯,都是xxx的组件名开头,防止最终合并项目时,需要手动解决冲突。
-
ButterKnife
使用
ButterKnife
的时候,我们在主APP使用是没有任何问题的,在Library中使用会出现一些问题在library中的BindView(R.id.xx)时,会发现R文件不是一个
final
常量,在编译library为aar的时候,R.java文件还不是一个具体的常量值,最终编译classes的时候,会使用常量值替换R文件的变量,所以由于R文件不是具体的常量,使用butterKnife的时候,是无法使用注解和swtichcase的。
butterKnife给出了具体的解决方案
-
build.gradle加入插件依赖
buildscript { dependencies { classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0' } }
-
在组件的build.gradle中引入
apply plugin: 'com.jakewharton.butterknife'
-
引入之后我们就可以在组件的开发中 使用R2文件来使用注解了
class ExampleActivity extends Activity { @BindView(R2.id.user) EditText username; @BindView(R2.id.pass) EditText password; }
-
-
swtich-case
swtich-case: R2只能使用在注解中,当我们注册多个view的点击事件的时候,没法使用R2,
只能使用if else来处理逻辑了
待优化的地方
-
给每个组件设置独立的isRunApp的变量值,分开控制,这样在调试的时候,是可以运行主app调试,但是组件只依赖两个相关的,减少运行调试等待的时间;
总结
-
对于组件化重点是对业务模块的划分,对于搭建流程没什么复杂。组件化开发并不是为了组件化而去组件化,主要我们需要根据项目的开发形式,和对业务模块的理解来决定是否组件化。引用一句针对组件化文字的评论:
组件化难的是怎么拆分基础模块,需求在不断变化,有时候以前觉得很好的方法,到最后反成限制了
-
组件化实战中,会有很多问题需要解决,这里在写demo,可能会有很多的问题没有暴露出来。
-
对于单个组件的调试,不能作为最终项目的调试结果,实际开发中,组件自测没问题后,我们最终一定要以app运行,去测试我们的项目,环境不一致,可能会引发想不到的问题。
文档汇总
组件化框架
-
得到DDComponentForAndroid
-
MvpArms
MVPArms 是一个整合了大量主流开源项目的 Android MVP 快速搭建框架, 其中包含 Dagger2、Retrofit、RxJava 以及 RxLifecycle、RxCache 等 Rx 系三方库,
组件化相关文章
作者介绍
- 冀旭晖 Android 高级开发工程师