Android 组件化开发

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使用,所以只会使用该module下res下的资源文件,但当在release模式下,
会将整个工程作为一个application,因此所有的资源文件都会被合并到一起,如果module里的有资源文件同名,则会被覆盖,因此有可能
造成错误

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

resourcePrefix “前缀_”

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

依赖版本

依赖

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

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

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

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

在Application一节中,我们使用了一个commonsdk的module,用来给上游的module提供application,在这个module中,我们也可以在这个commonsdk的module中添加上游的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"
    
}

然后将各个module的build.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,那可以在app的dependencies中
    依赖各个module的aar文件,这个aar文件可以在各个module的/build/outputs/aar下找到,可以写脚本或者
    手动将各个module生成的aar复制到app的libs文件夹下,并进行引用。

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里的host和scheme,用隐式调用进行跳转。
    例如,在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);

最后放一张最终的架构图
note_android_modularity_architecture.png

作者

PPTing

发布于

2017-12-24

更新于

2022-02-12

许可协议

评论