Android 组件化开发

在项目的开发中,业务模块越来越多,代码量越来越多,编译构建的时间也越来越长,尝试将项目进行组件化开发。

所谓组件化,就是将各个业务模块解耦,在开发的时候将每个业务模块当做单独的 application 开发,在开发完毕后打包成 aar 或者以 module 的形式依赖到主 application 中。

首先,在项目根目录下的gradle.properties文件里设置一个全局变量 isDebug

  1. true ,即debug模式,每个 module 为单个 application
  2. false,即非debug 模式,将app主模块编译为 app,并将其他 module 依赖进 app

AndroidManifest

在每个module中的main文件夹下建立两个文件夹,一个是debug,一个是release
用来存放AndroidManifest文件

  1. debug 模式下,每个module都是单独的app
    所以在AndroidManifest中需要添加一个activity作为启动activity
1
2
3
4
5
6
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
  1. release 模式下,每个module都是主app的一个依赖,可以不需要启动activity
    如果需要从主app中打开这些activity,可以使用隐式调用或者使用路由表去启动(放在后面再说)

build.gradle

  1. 在主app中,不论在 debug 或者 release 模式下都作为application存在,

    apply plugin: ‘com.android.application’

  2. 在各个module中,在 debug 模式下,作为application,在 release 模式下,作为library

1
2
3
4
5
if (isDebug.toBoolean()){
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
  • AndroidManifest合并的问题

在各个module中,利用sourceSets(在android标签下)中设置每个模式中的AndroidManifest文件

1
2
3
4
5
6
7
8
9
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}

Application

我们知道,一个应用在启动后,系统会为其创建一个applicaiton对象,这个application对象的生命周期就等于整个应用的生命周期,
一般我们会在里头定义一个全局的context
但是,如果在组件化开发中,在debug模式下,每个module是作为application存在的,但当我们打包时,即release模式下,
每个module是作为library存在的,这时如果使用该module里的getApplicationContext()就会报错,因为release模式下
只有一个application

解决方案:

我们在AndroidManifest合并的问题中,在sourceSets里指定AndroidManifest的路径,
我们也可以指定在release模式下排除掉某些文件

1
2
3
4
5
6
7
8
9
10
11
12
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
java {
exclude 'debug/**'
}
}
}
}

所以我们可以在debug文件夹里的AndroidManifest文件可以指定该Application,而在release文件夹下的AndroidManifest中不指定Application
但是这种方法也有局限,因为这样的话,我们的module里就没法使用全局的context对象了。

为了解决上面这个问题,我们将 application 定义在最底层的 module 中(取名为 commonSdk),并在最上层的 app 中的 AndroidManifest 中去使用这个 application ,这样所有的 module 就都可以使用这个 application context 对象了。

资源文件

在开发过程中,我们将module作为一个application使用,所以只会使用该moduleres下的资源文件,但当在release模式下,
会将整个工程作为一个application,因此所有的资源文件都会被合并到一起,如果module里的有资源文件同名,则会被覆盖,因此有可能
造成错误

为了解决这个问题,我们可以在所有的资源文件的名字上加个前缀,用module区分开来,则不会产生上述问题,
gradle中,可以用

resourcePrefix “前缀_”

强制在资源文件名字上加上该前缀,否则会编译不通过,但这个方法不会限定drawable里的资源文件,因此在drawable中的资源文件命名需要在开发过程中加以规范

依赖版本

依赖

Android开发中,我们会使用很多的依赖库,在非组件化的项目中,我们只需要一股脑将依赖全部写在appbuild.gradle
文件中,这样我们的项目就可以用这些依赖了。

但是在组件化中,如果我们也按照这种方式,那就有可能造成重复的依赖,如果我们的firstmodulesecondmodule中都需要http请求,则都去依赖某个http请求库,如果是使用compile 'libiray_name:version'这种方式去依赖,gradle会自动帮我们选出最新的版本去依赖,如果两个module中使用的是不同版本的依赖,并且某个新版本中删除了一些api,如果依赖了旧版本的module使用了这些在新版本中被删除的api,那就会报错。

另外,如果我们的module中是使用compile project(':project')这样的依赖,gradle不会帮我们去重,最后打包后代码
里就会有重复的类。

那如何去解决这个问题呢?

Application一节中,我们使用了一个commonsdkmodule,用来给上游的module提供application,在这个module中,我们也可以在这个commonsdkmodule中添加上游的module所需要的依赖,比如http请求库(Retrofit),Gson等等,这样在上游的各个module就都能使用这些依赖。而现在我们只需要在commonsdk这个module中去管理我们的依赖,比如添加、删除、升级依赖。这样我们的主app就不需要去重复依赖一些库了,主app只需要依赖commonsdk这个module,而这个module已经提供了需要的依赖。

版本

如何去管理这些依赖的版本呢,可以在主工程目录下定义变量,进行统一管理
build.gradle中定义一个标签ext

ext{
    compileSdkVersion =  25
    buildToolsVersion = "25.0.2"
    minSdkVersion = 15
    targetSdkVersion = 25

    supportLibraryVersion = "25.3.1"
    espressoCoreVersion = "2.2.2"
    junitVersion = "4.12"
    retrofitVersion = "2.2.0"

}

然后将各个modulebuild.gradle里的变量都通过rootProject.ext.xxx取值

如果是dependencies的话,则用$rootProject.version取值

例如

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile("com.android.support.test.espresso:espresso-core:$rootProject.espressoCoreVersion", {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
    testCompile "junit:junit:$rootProject.junitVersion"
    compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
}

debug 模式下依赖

按照我们上述的想法去做以后,或许还存在一个问题,就是在debug模式时,我们的各个module是作为application存在的,
这个时候如果我们想安装整个项目app,按照我们的这种模式是无法打开我们的各个业务module的,那难道这个问题就没法解决
了吗?
当然不是。

  1. 我们可以修改为release模式,这样就可以将各个module作为library进行依赖,也就可以打开业务模块了
    看到这里你们可能会说 你再逗我吗 = =

  2. 如果我们想在debug模式下也能安装app的话,并且能够打开各个业务module,那可以在appdependencies
    依赖各个moduleaar文件,这个aar文件可以在各个module/build/outputs/aar下找到,可以写脚本或者
    手动将各个module生成的aar复制到applibs文件夹下,并进行引用。

1
2
3
4
5
6
7
android{
repositories {
flatDir {
dirs 'libs'
}
}
}

然后在dependencies进行依赖

dependencies {
    if(isDebug.toBoolean()){
        compile project(':router')
        compile project(':commonsdk')
        compile(name:'firstmodule-release', ext:'aar')
        compile(name:'secondmodule-release', ext:'aar')
    }else {
        compile project(':firstmodule')
        compile project(':secondmodule')
    }
}

这样我们就可以在debug模式下也将整个工程运行起来了,当然,这样其实就没有意义了,因为我们组件化的目的就是为了能够
将各个业务module作为application运行,而不是运行整个app,以加快我们的编译速度等,而这样的话就失去了不需要
全部编译的意义了。

多 Product Flavors 模式下的依赖

本地 AAR 的依赖

在组件化的开发中,有时候遇到某个 lib 需要依赖某个本地 aar 文件
则在 dependencies 添加对本地的 aar 依赖,并将 aar 依赖文件放入 libs 目录下

1
compile(name:'aar_file_name', ext:'aar')

如果此时有多个 Flavors ,则在 compile 前添加 Flavor 名字,如

1
2
3
flavors1Compile(name: 'aar_file_name', ext: 'aar')
flavors2Compile(name: 'aar_file_name', ext: 'aar')
flavors3Compile(name: 'aar_file_name', ext: 'aar')

但如果这个库以同样的方式被依赖的话,会因为找不到这个文件而报错

1
Error:Failed to resolve: :arr_file_name: Open File

这时可以在项目的 build.gradle 中配置

1
2
3
4
5
6
7
8
9
10
allprojects {
repositories {
flatDir {
// 由于Library module中引用了 lib 库的 aar,在多 module 的情况下,
// 其他的module编译会报错,所以需要在所有工程的repositories
// 下把Library module中的libs目录添加到依赖关系中
dirs project(':lib_name').file('libs')
}
}
}

跳转 router

在不同的module中,要实现跨module之间的跳转,

  1. 可以使用intent并设置data里的hostscheme,用隐式调用进行跳转。
    例如,在AndroidManifest中设置
1
2
3
4
5
6
7
8
9
10
<activity
android:exported="false" //设置其他 application 不能唤起我们的 application
android:name="me.ppting.secondmoudle.MainActivity">
<intent-filter>
<action android:name="me.ppting.jump"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:host="ppting.me"
android:scheme="second"/>
</intent-filter>
</activity>

要进行跳转时,用下面的代码进行跳转

1
2
3
4
Intent intent = new Intent();
intent.setAction("me.ppting.jump");
intent.setData(Uri.parse("second://ppting.me"));
startActivity(intent);

最后放一张最终的架构图
架构