摘要
随着App业务的发展,业务模块会越来越多,而不同模块可能对某一个功能都有依赖,比如分享、登录等,这个时候如果没有将这些通用的功能抽象成组件单独拎出来,就面临着相同功能的代码在不同的模块中都有一分实现,如果需要更改这个功能的实现,就需要在每个模块中都改一遍,因此需要将这些通用功能抽象成组件独立出来,这就是组件化的过程。
结构
切换到项目根目录,使用tree -d -L 1
命令查看项目的结构。
1 | . |
要解决的问题
组件化的过程中通常会遇到以下这些问题:
- 组件单独调试,即在没有集成到APP壳子工程上去之前,我们可以单独编译、运行、调试对应组件;
- 组件通信,这里说的通信,指的是APP壳子工程和组件、组件与组件之间的通信,包括页面访问、数据传递;
- 组件集成,在各个组件开发好后,如何方便的集成到APP壳子中去;
- 组件容错,集成是在某个被集成的组件没有开发好的情况下如何友好的容错;
- 组件解耦与代码隔离,这里的接口要求组件A中不直接出现组件B中的某个类的引用
组件单独调试
Android Studio采用的是Gradle来构建项目的,对于一个module,可以对其应用以下插件:
- APP插件,对应插件id为
com.android.application
,用来配置一个Android App工程,构建后输出一个apk安装包; - Library插件,对应插件id为
com.android.library
,用来构建一个Android Library工程,构建后输出一个aar包; - Test插件,对应插件id为
com.android.test
,用来构建一个Android Test工程;
显然,单独调试时就要采用APP插件,被别的module依赖时就要采用Library插件,所以只要动态的设置插件id就可以了,而动态配置这个插件id对于gradle来说是超简单的。项目根目录下新建一个base_component.gradle文件,加入如下内容:
1 | // 基本组件的配置信息 |
根目录下的配置文件config.gradle内容如下:
1 | ext { |
有了上面的配置后,如果一个组件如image需要集成进App壳子工程中,取消merge数组中image的注释即可,如果单独运行,注释起来即可;
组件通信
组件通信包括组件间的参数传递,页面跳转,这里将两种实现方式,一种是基于接口+实现的方式来实现的,一种是使用ARouter来实现的。
接口+实现来完成组件通信
为了演示通过接口+实现来完成组件通信,这里新建两个组件:component-music、component-video,分别对应登录组件和分享组件,以及一个用来实现通信的component-base 库,其实最终依赖的是Java的反射机制。
component-base库
新建一个类型为Library的component-base的module(名字可以随便取了),改库的职责就是集中处理各个组件需要暴露出来的服务(service),比如music和video要暴露哪些方法给其他组件调用,都可以以接口的形式在这里定义,然后相应的组件中实现这些接口。这里假设music组件和video组件要暴露的服务(即接口)分别为IMusicService和IVideoService,其定义如下:
1 | // IMusicService.java |
上面的两个定义在component-base
这个module中,为了兼容(即避免service没有获取到的情况),我在component-base
中提供了连个接口的默认实现(空实现)。内容如下:
1 | // DefaultVideoService.java |
上面四个文件定义好后,我们就要考虑如何几种管理了,这里我新建了一个ServiceFactory,用来管理各组件暴露给外部的服务。内容如下:
1 | //ServiceFactory.java |
看下component-base现在的基本结构:
1 | com.rainmonth.componentbase |
到此,component-base的基本职责就实现了。
服务的注册
定义好ServiceFactory后,我们就要想办法把各个component自己实现的service注册到ServiceFactory中,前面提到了各个component都依赖于component-base,所以我们是要在component实现了ServiceFactory中定义好接口,然后调用相应的set方法将其引用传递给ServiceFactory即可。
1 | // component-music下的MusicServie.java |
经过上面一波操作,各个组件的接口服务就已经注册到ServiceFactory中了,只要在需要调用的得房调用ServiceFactory的get方法然后调用即可。如:
1 | // music下的随便一个Activity |
组件Application动态配置问题解决
上面接口服务的注册放在了各个module的Application创建时,但当组件作为子module被主module所依赖是,由于Application的替换原则,组件的Application根本不会被创建,因为就不能注册进去了。当然我们可以在主module的Application中(这里就叫MainApplication)持有各个子module的Application的强引用,然后在调用相关的方法完成注册,但这就耦合起来了。可以采用反射机制来避免强引用。具体操作如下:
在BaseApplication中定义一个抽象方法,用来供module实现(包括主module和子module):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//BaseApplication.java
public abstract class BaseApplication extends Application {
private BaseApplicationDelegate mBaseApplicationDelegate;
public void onCreate() {
super.onCreate();
...
}
/**
* 这是个模板方法,库module只负责实现,真正的调用发生在主module中
* 初始化module对外提供的服务
*/
public abstract void initModuleService();
}所有的组件module都继承BaseApplication,并实现其定义的方法,同时将服务注册给自己,方便独立运行时使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// MusicApplication.java
public class MusicApplication extends BaseApplication {
public void onCreate() {
super.onCreate();
initModuleService();
}
public void initModuleService() {
ServiceFactory.getInstance().setMusicService(new MusicService());
}
}
// VideoApplication.java
public class VideoApplication extends BaseApplication {
public void onCreate() {
super.onCreate();
initModuleService();
}
public void initModuleService() {
ServiceFactory.getInstance().setVideoService(new VideoService());
1 | 3. 集中各个module的Application类,以方便遍历反射。这里采用在common库中定义一个ServiceConfig类,内容如下: |
后面如果其他组件有类似服务,只要将其所属的Application的类路径加入到appMoudles数组即可。
- 主Module中继承BaseApplication,实现并调用BaseApplication的中的抽象方法,内容如下:
1 | // MainApplication.java |
至此,我们就通过接口+实现的方式解决了组件通信的问题了。
ARouter完成组件通信
关于使用ARouter来完成组件间的通信,可以参看Android ARouter使用及源码分析一文来具体了解。
这里就简单介绍下其使用。
添加注解
假设APP壳子中有一个需要调整到component-music下的MusicMainActivity,那么我们需要按如下方式添加注解:
1 | /** |
这里的path是RouterConstant下定义的PATH_MUSIC_HOME常量,其值为/music/home
要求至少要有两级,一个表示group,一个表示具体的页面。这里建议将所有的ARouter要使用的PATH值几种设置在route这个module下,方便管理。
使用注解
上面的注解添加完成后,可以调用在要进行通信的地方按如下方式调用:
1 | RouterUtils.getInstance().build(RouterConstant.PATH_MUSIC_HOME).navigation(); |
上面的RouterUtils为ARouter的简单封装,内容如下:
1 | package com.rainmonth.router; |
这样就可以完成组件间的页面跳转了,当然在上面调用navigation时可以传递参数,这里就不详述了。
组件集成
组件集成很简单,将想要集成的组件名字(去掉component-前缀)加入到config.gradle的merge数组中即可将其集成到App module中。
组件容错
上面的实例中已经提到了,可以在服务获取不要是提供一个空实现,或者给与提示,这样就可以避免由于找不到服务而导致App crash。
组件解耦
这里说的解耦指的是主module在不直接访问组件类的情况下使用组件提供的服务,看到上面的接口+实现方法解决通信问题后,很资源的就想到可以用反射来解决这个问题,原理类似,这里就不再赘述了。
总结
以上就是我组件化的一个实践,主要涉及到了一些gradle的基本配置、反射解耦及接口+实现的方式解决通信问题,其实组件化并没有那么复杂。