项目实战之组件化架构

3,480 阅读8分钟

前言

关于什么是组件化、为什么要进行组件化以及实施组件化的基本流程网上一搜一大把,这里不做过多说明,不了解的话可以Google一下。这里主要记录一下组件化开发的一些心得和踩的一些坑。

先看一下项目结构图

结构很简单,有一个公共的基础module类commonlibrary来处理一些公共的东西,比如第三方库的依赖,基类封装,工具类等。中间层是各个独立的业务模块,各个模块之间互相隔离。最下面是app的壳,主要配置签名打包什么的。具体可以看一下demo

组件化实施步骤

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路由跳转,项目的地址github.com/alibaba/ARo…

这里需要特别说明一下,需要跳转的目标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做一下桥接,了解更多可以参考这里。不过感觉用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是时候懂了

(6)其他问题本篇博客会持续更新……

2018年6.15更新………………………………………………………… 编译报错

 Multiple dex files define Lcom/alibaba/android/arouter/routes/ARouter?Group?module

一般网上说是依赖版本冲突,其实这个问题是不同module之间有相同分组导致的问题,比如a模块 path = "/message/a",b模块 path = "/message/b",有相同的message分组,修改成不一样的就可以了。

2018.8.20更新…………………………………………………………

最近在用kotlin和java混合开发,发现原有java页面跳转新写的kotlin页面 arouter 页面跳转的时候报异常提示 There is no route match the path……,此时参考官方文档即可解决,

// 在kotlin的module中添加插件
apply plugin: 'kotlin-kapt'
// 依赖里 使用kapt 引用
dependencies {
    compile 'com.alibaba:arouter-api:x.x.x'
    kapt 'com.alibaba:arouter-compiler:x.x.x'
    ...
}
2018.8.22更新…………………………………………………………

遇到了一个很蛋疼的问题,在纯java写的module里通过arouter跳转到另一个module里的kotlin页面的时候,发现setcontentview方法无效,页面什么都不显示。调试了半天,发现是页面的xml布局文件和一个空的xml布局文件重名了,导致kotlin页面加载了空页面的布局,在此记录一下,好尴尬。

最后附上完整的demo地址,如果对你有帮助麻烦start鼓励一下,你的鼓励是我前进的动力。