什么是组件化
项目按功能拆分成功若干个组件,每个组件负责相应的功能,如login、pay、live。组件化与模块化类似,但不同的是模块化是以业务为导向,组件化是以功能为导向。组件化的颗粒度更细,一个模块里可能包含多个组件。实际开发中一般是模块化与组件化相结合的方式。
为什么要组件化
(1)提高复用性避免重复造轮子,不同的项目可以共用同一组件,提高开发效率,降低维护成本。
(2)项目按功能拆分成组件,组件之间做到低耦合、高内聚,有利于代码维护,某个组件需要改动,不会影响到其他组件。
组件化方案
组件化是一种思想,团队在使用组件化的过程中不必拘泥于形式,可以根据自己负责的项目大小和业务需求的需要制定合适的方案,如下图就是一种组件化结构设计。
- 宿主app
在组件化中,app可以认为是一个入口,一个宿主空壳,负责生成app和加载初始化操作。 - 业务层
每个组件代表了一个业务,组件之间相互隔离解耦,方便维护和复用。 - 公共层
既然是base,顾名思义,这里面包含了公共的类库。如Basexxx、Arouter、ButterKnife、工具类等 - 基础层
提供基础服务功能,如图片加载、网络、数据库、视频播放、直播等。
注:以上结构只是示例,其中层级的划分和层级命名并不是定性的,只为更好的理解组件化。
组件化面临的问题
- 跳转和路由
Activity跳转分为显示和隐示:
//显示跳转
Intent intent = new Intent(cotext,LoginActivity.class);
startActvity(intent)
//隐示跳转
Intent intent = new Intent();
intent.setClassName("app包名" , "activity路径");
intent.setComponent(new Component(new Component("app报名" , "activity路径")));
startActivity(intent);
1、显示跳转,直接依赖,不符合组件化解耦隔离的要求。
2、对于隐示跳转,如果移除B的话,那么在A进行跳转时就会出现异常崩溃,我们通过下面的方式来进行安全处理
//隐示跳转
Intent intent = new Intent();
intent.setClassName("app包名" , "activity路径");
intent.setComponent(new Component(new Component("app报名" , "activity路径")));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
startActivity(intent);
原生推荐使用隐示跳转,不过在组件化项目中,为了更优雅的实现组件间的页面跳转可以结合路由神器ARouter,ARouter类似中转站通过索引的方式无需依赖,达到了组件间解耦的目的。
Aouter使用方式如下:
1、因为ARouter是所有模块层组件都会用到所以我们可以在Base中引入
api 'com.alibaba:arouter-api:1.5.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
2、在每个子module里添加
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
annotationProcessor会通过javaCompileOptions这个配置来获取当前module的名字。
3、在Appliction里对ARouter进行初始化,因为ARouter是所有的模块层组件都会用到,所以它的初始化放在BaseAppliction中完成。
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initRouter(this);
}
public void initRouter(Application application) {
if (BuildConfig.DEBUG) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); //打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(application); //尽可能早,推荐在Application中初始化
}
}
4、在Activity中添加注解Router
public interface RouterPaths {
String LOGIN_ACTIVITY = "/login/login_activity";
}
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = RouterPaths.LOGIN_ACTIVITY)
public class LoginActivity extends BaseActivity {
}
path是指跳转路径,要求至少两级,即/xx/xx的形式,第一个xx是指group,如果不同module中出现相同的group会报错,所以建议group用module名称标识。
5、发起跳转操作
ARouter.getInstance().build(RouterPaths. LOGIN_ACTIVITY).navigation();
ARouter的还有很多其他功能,这里不作详细说明。
- Aplication动态加载
Application作为程序的入口通常做一些初始化,如上面提到的ARouter,由于ARouter是所有模块层组件都要用到,所以把它放在BaseApplication进行初始化。如果某个初始化操作只属于某个模块,为了降低耦合,我们应该把该初始化操作放在对应模块module的Application里。如下:
1、在BaseModule定义接口
public interface BaseApplicationImpl {
void init();
...
}
2、在ModuleConfig中进行配置
public interface ModuleConfig {
String LOGIN = "com.linda.login.LoginApplication";
String DETAIL = "com.linda.detail.DetailApplication";
String PAY = "com.linda.pay.PayApplication";
String[] modules = {
LOGIN, DETAIL, PAY
};
}
3、在BaseApplicatiion通过反射的方式获取各个module中Application的实例并调用init方法。
public abstract class BaseApplication extends Application implements BaseApplicationImpl {
@Override
public void onCreate() {
super.onCreate();
initComponent();
initARouter();
}
/**
* 初始化各组件
*/
public void initComponent() {
for (String module : ModuleConfig.modules) {
try {
Class clazz = Class.forName(module);
BaseApplicationImpl baseApplication = (BaseApplicationImpl) clazz.newInstance();
baseApplication.init();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
...
}
4、子module中实现init方法,并进行相关初始化操作
public class LiveApplication extends BaseApplication {
public void init() {
//在这里做一些的Live相关的初始化操作
}
}
- 模块间通信
BroadcastReceiver:系统提供,比较笨重,使用不够优雅。
EventBus:使用简单优雅,将发送这与接收者解耦,2.x使用反射方式比较耗性能,3.x使用注解方式比反射快得多。
但是有些情况是BroadcastReceiver、EventBus解决不了的,例如想在detail模块中获取mine模块中的数据。因为detail和mine都依赖了base,所以我们可以利用base来实现
1、在base中定义接口并继承ARouter的IProvider。
public interface IMineDataProvider extends IProvider {
String getMineData();
}
2、在mine模块中新建MineDataProvider类实现IMineDataProvider,并实现getMineData方法
@Route(path = RouterPaths.MINE_DATA_PROVIDER)
public class MineDataProvider implements IMineDataProvider {
@Override
public String getMineData() {
return "***已获取到mine模块中的数据***";
}
@Override
public void init(Context context) {
}
}
3、在detail中获取MineDataProvider实例并调用IMineDataProvider接口中定义的方法
IMineDataProvider mineDataProvider = (IMineDataProvider) ARouter.getInstance().build(RouterPaths.MINE_DATA_PROVIDER).navigation();
if (mineDataProvider != null) {
mGetMineData.setText(mineDataProvider.getMineData());
}
- 资源冲突
组件化项目中有很多个module,这就难免会出现module中资源命名相同而引起引用错误的情况。为此我们可以在每个module的build.gradle文件进行如下配置(例如login模块)。
resourcePrefix "login_"
所有的资源必须以指定的字符串(建议module名称)做前缀,不然会报错。不过这种方式只限定与xml文件,对图片资源无效,图片资源仍需要手动修改。
//布局文件命名示例
login_activity_login.xml
Login
- 单个组件运行调试
当项目越来越庞大时,编译或运行一次就需要花费很长时间,而组件化可以通过配置对每个模块进行单独调试,大大提高了开发效率。
我们需要对每个module进行如下配置:
1、新建common_config.gradle文件并声明变量isModuleDebug;
project.ext {
//是否允许module单独调试
isModuleDebug = false
}
2、引入common_config配置,另外因为组件化中每个module都是一个library,如要单独运行调试需要将library换成application,在module的build.gradle中文件中做如下修改:
//引入common_config配置
apply from: "${rootProject.rootDir}/common_config.gradle"
if (project.ext.isModuleDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
defaultConfig {
if (project.ext.isModuleDebug.toBoolean()) {
// 单独调试时需要添加 applicationId
applicationId "com.linda.login"
}
...
}
sourceSets {
main {
//在需要单独调试的module的src/main目录下新建manifest目录和AndroidManifest文件
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (project.ext.isModuleDebug.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
关于两个清单文件的不同之处如下:
3、如果module单独调试,那么在app就不能再依赖此module,因为此时app和module都是project,project之间不能相互依赖,在app的build.gradle文件中做如下修改
dependencies {
if (!project.ext.isModuleDebug) {
implementation project(path: ':detail')
implementation project(path: ':login')
implementation project(path: ':pay')
}
implementation project(path: ':main')
implementation project(path: ':home')
implementation project(path: ':mine')
}
4、最后将isModuleDebug改为true,然后编译,便可以看到login、detail、pay模块可以独立运行调试了。
组件化Demo下载地址