前言
关于什么是组件化, 为什么要进行组件化以及实施组件化的基本流程网上一搜一大把, 这里不做过多说明, 不了解的话可以 Google 一下. 这里主要记录一下组件化开发的一些心得和踩的一些坑.
先看一下项目结构图
结构很简单, 有一个公共的基础 module 类 commonlibrary 来处理一些公共的东西, 比如第三方库的依赖, 基类封装, 工具类等. 中间层是各个独立的业务模块, 各个模块之间互相隔离. 最下面是 app 的壳, 主要配置签名打包什么的. 具体可以看一下 https://github.com/lxykad/article_demos .
组件化实施步骤
1, 设置 module 是否作为组件的开关
在 gradle.properties 文件里定义一个常量 IsBuildApp = false, 表示是否把组件 module 作为单独的 app 运行. 定义好了这个常量后, 在项目的任何一个 gradle 文件里都可以读取到这个值, 那么就用这个值来作为 module 组件是否需要单独运行的开关.
- // 在 module 组件的 gradle 里配置如下, gradle.properties 中的数据类型都是 String 类型, 这里需要做一下转换
- if (IsBuildApp.toBoolean()){
- apply plugin: 'com.android.application'
- }else {
- apply plugin: 'com.android.library'
- }
2, 组件 module 的清单文件 AndroidManifest 合并问题
我们知道 android 的四大组件, 权限等都是需要注册的, 当 module 单独运行的时候, 肯定需要一个清单文件注册组件和申请权限, 但是当 module 作为 app 的一个子组件存在的时候, 清单文件是要合并到 app 的壳工程中的, 这个时候如果每个 module 都有自己的启动页面和自定义 application 的话, 就会引起冲突.
为了解决这个问题, 那就需要根据 module 是否需要单独运行来配置不同的清单文件. 在 java 同级目录新建 independent 目录, 在此目录下创建项目 module 需要单独运行的清单文件和 application. 然后在 module 的 gradle 文件里指定清单文件路径, 代码如下:
- // 在 android 领域里指定清单文件的路径
- sourceSets {
- main {
- if (IsBuildApp.toBoolean()) {
- // 单独作为 app 运行的清单文件, 这里可以添加启动页面, 自定义 application 等.
- manifest.srcFile 'src/main/independent/AndroidManifest.xml'
- } else {
- // 作为组件的清单文件
- manifest.srcFile 'src/main/AndroidManifest.xml'
- //release 模式下排除 independent 文件夹中的所有 Java 文件
- java {
- exclude 'independent/**'
- }
- }
- }
- }
这样配置完成以后, 作为组件的清单文件是不能有自己的启动页面, application,appname 等属性的, 下面看一下完整的配置:
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.article.demos.vue">
- <application android:theme="@style/AppTheme">
- <activity android:name=".ui.VueActivity" />
- </application>
- </manifest>
下面看一下独立运行模式下的清单文件:
- // 作为独立 app 运行的清单文件, 注意这里我设置了主题, 不然的话会报错.
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.article.demos.main">
- <application android:theme="@style/AppTheme">
- <activity android:name=".MainActivity">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- </manifest>
独立运行的话, 就和正常的 app 清单文件一样, 要有启动页面, application 标签可以添加 label,icon, 自定义 application 等, 就不多说啦.
3, 全局 Application 的问题
在 commonlibrary 中创建自定义 application, 因为其他的 module 都依赖这个 module, 所以其他的 module 都可以获取到这个全局的 application. 另外, 组件在独立运行模式下的 application, 继承我们自定义这个 BaseApplication 就可以了. 因为我们在 release 模式下, 排除了所有 independent 文件夹下的 java 文件, 所以作为组件运行时, 并不会产生 application 的冲突, 配置如下:
- sourceSets {
- main {
- if (IsBuildApp.toBoolean()) {
- manifest.srcFile 'src/main/independent/AndroidManifest.xml'
- } else {
- manifest.srcFile 'src/main/AndroidManifest.xml'
- //release 模式下排除 independent 文件夹中的所有 Java 文件
- java {
- exclude 'independent/**'
- }
- }
- }
- }
4, 重复依赖三方库的问题
为了避免重复依赖三方库的问题, 我们的三方库依赖统一放在 commonlibrary 的 module 中, 这样既可以避免重复依赖, 又方便管理. 然后我们在 app 的 module 里, 如下引用即可:
- dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
- if (IsBuildApp.toBoolean()) {
- implementation project(':commonlibrary')
- } else {
- implementation project(':androidmodule')
- implementation project(':vuemodule')
- implementation project(':kotlinmodule')
- implementation project(':javamodule')
- }
- }
5, 资源冲突问题
资源冲突主要是指各个 module 里的资源文件名冲突的问题, 如果命名一样, 合并的时候便会产生冲突.
解决冲突主要有两个解决方案, 一个是约定规则, 比如资源名约定都以 module 名开头.
方案二是通过 gradle 脚本来设置, 在各个组件的 gradle 文件里添加如下代码:
resourcePrefix "module 名称_"
但是这种配置有限制, 比如只能限定 xml 里的资源, 所以并不推荐这种方式.
6, 组件间跳转
因为组件是相互隔离的, 我们并不能显式跳转, 这里我们选用阿里巴巴的 Arouter 路由跳转, 项目的地址 https://github.com/alibaba/ARouter .
这里需要特别说明一下, 需要跳转的目标 module 需要引入 arouter 的注解处理器, 否则无法处理 router 注解会出现路径不匹配的问题:
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
同时, 改 module 的 defaultconfig 里也别忘记配置 moduleName
- javaCompileOptions {
- annotationProcessorOptions {
- arguments = [ moduleName : project.getName() ]
- }
- }
7, 跨 module 交互
跨 moduel 交互一般是指 module 间通信和 module 间的相互调用. module 间通信这里选用 eventbus, 很简单, 就不过多说明了.
下面说一下同级 module 直接的通信, 比如我在任何一个页面要调用 loginModule 里的微信登录方法, 因为各个 module 是互相独立的, 互不依赖, 想要直接调用基本不可能. 目前网上发现有两种解决方案, 一个是写一个反射工具类, 通过反射获取到要调用的类, 然后调用相应的方法. 另一个是通过 commonModule 做一下桥接, 了解更多可以参考这里. https://www.jianshu.com/p/3faa835de543 不过感觉用 Arouter 能更优雅的实现, 下面具体讲一下利用 arouter 来实现.
首先, 在公共 module 里创建一个接口 IService
- public interface IService extends IProvider{
- String wxLogin();
- }
接口里定义一个微信登录的伪代码, 然后在我们的登录组件里, 实现该接口并添加 route 注解
- @Route(path = Constant.WX_LOGIN)
- public class WxTest implements IService{
- @Override
- public void init(Context context) {
- }
- @Override
- public String wxLogin() {
- return "wxlogin";
- }
- }
其中 Constant.WX_LOGIN 是我定义的一个字符串常量
public static final String WX_LOGIN = "/wx/login";
以上两步就把工作做完了, 下面只需要在需要调用的页面调用登录就行了. 首先, 我们获取到 IService
- /**
- * 推荐使用方式二来获取 IService
- */
- // IService iService = (IService) ARouter.getInstance().build(Constant.WX_LOGIN).navigation();
- IService iService = ARouter.getInstance().navigation(IService.class);
拿到 IService 后, 就可以放心大胆的调用登录方法就行了.
- mBinding.btLogin.setOnClickListener(v -> {
- String s = iService.wxLogin();
- Toast.makeText(getContext(), s, Toast.LENGTH_SHORT).show();
- });
8,fragment 的组件化
一般的项目首页都是一个 activity 和多个 fragment 组成. 由于组件间的隔离, 我们在首页里怎么获取到其他组件里的 fragment 呢? 开篇的两个参考文章分别使用了两种不同的方式, 有兴趣的朋友可以看看. 各有利弊吧, 一个是查询所有, 太耗时. 一个是直接反射获取, 但是好像有点违背组件隔离, 需要知道 fragment 的全路径.
这里我参考了Android 组件化架构一书, 使用 arouter 来获取. 其实三种方式获取的原理一样, 都是通过反射. 我们看一下 arouter 的注解的源码就知道:
- @Target({ElementType.TYPE})
- @Retention(RetentionPolicy.CLASS)
- public @interface Route {......}
可以看到 Route 注解的 retention 是 CLASS, 也是通过反射来获取.
9, 遇到的一些坑
(1) 使用 dataBinding 的话, 每个 module 的 gradle 文件里都要加上 dataBinding 的支持, 否则无法生成相应的 binding 类
- // 每个 module 都加上 dataBinding 的支持, 否则无法生成相应的 binding 类
- dataBinding {
- enabled = true
- }
(2)java8 的支持一样要每个 module 都要单独配置
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
(3) 升级到 as 3.1.2 后, 出现无法访问 TaskStackBuilder 的问题
检查一下你的 support 包, 将你的 support 包更新到 27 或以上即可.
(4) 如果使用有自定义注解 annotation 的话, 如果编译报错 Annotation processors must be explicitly declared now..., 那么在 commonlibrary 的 gradle 文件的 defaultConfig 里添加如下代码:
- // Annotation processors must be explicitly declared now
- javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
(5) 如果你组件化开发, 子 module 中无法使用 butterknife 的话, 网上自行搜解决方案吧 ()
关于为何出现这个问题, 推荐一篇博文 R.java,R2.java 是时候懂了 http://www.wangyuwei.me/2017/06/15/R-java、R2-java是时候懂了/
(6) 其他问题本篇博客会持续更新......
最后附上完整的 demo 地址 https://github.com/lxykad/article_demos , 如果对你有帮助麻烦 start 鼓励一下, 你的鼓励是我前进的动力.
来源: https://juejin.im/post/5aefff5ef265da0b767d6429