1.概述

早期用 Eclipse 进行 Android 开发,创建一个工程,对于引入开源框架时候,采用的是用库的形式进行引入;到后来有 Android Studio 的出现,在 Project 下可以存在多个 module,除了要运行的 module 是 application 外,其他 module 都是 library。在每个 module 的 build.gradle 文件中区分:

应用:

apply plugin: 'com.android.application'

库:

apply plugin: 'com.android.library'

2. 模块化和组件化

2.1 模块化

把常用的功能、控件、基础类、第三方库、权限等公共部分抽离封装,把业务拆分成N个模块进行独立(module)的管理;而所有的业务组件都依赖于封装的基础库,业务组件之间不做依赖,这样的目的是为了让每个业务模块能单独运行。模块化的特点是:模块之间解耦,可以独立管理。

如下项目采用模块化,只有一个模块是 application,其他的都是 libary:

android 模块化开发 android模块化和组件化_android 模块化开发

优缺点:

如上图所示,将自定义的软键盘、自定义控件等分为不同的模块,彼此之间降低耦合;而且模块可以复用,甚至不同的项目都可以复用。随着业务越来越多,模块也越来越多,当模块原来越多,每次进行编译都要长时间的等待。

2.2 组件化

将一个app的代码拆分成几份独立的组件,组件之间是低耦合的,可以独立编译打包;也可以将组件打包到一个apk中。组件化的核心是角色的转换。 在打包时, 是library; 在调试时, 是application。组件化后的优点:每个模块可独立编译,提高了编译速度;开发只负责自己的模块,还可以再做的隔离一些,每个业务线只可见自己业务模块的代码,避免了误修改和版本管理问题;公共的Lib依然是个独立模块。

组件化与模块化的区别:是每个模块的角色的转换,一个组件可以独立编译打包,也可以作为lib集成到整个apk中。

3.组件化的实践

最近瑞幸咖啡正处于风口浪尖,就以瑞幸咖啡的APP为模板:

android 模块化开发 android模块化和组件化_android_02

分为五个模块:首页、菜单、潮品、购物车、我的。

3.1 创建模块,并实现组件模式和集成模式的切换

创建五个module,但是五个 module 都是application,可以独立运行,这里对 application 模块名进行统一格式进行命名。对于application 模块以 module_X 的格式进行命名,而对于 library 模块以 lib_X 的格式进行命名。

android 模块化开发 android模块化和组件化_android_03

创建了三个不同的module,如何做到在调试阶段,每个module都能独立运行,在发布时候集成编译成一个APP呢?Android Studio中的Module主要有两种属性,分别为:application属性和library属性;在项目的根目录下的文件 gradle.properties 中定义相关常量,所有的 module 都能读取该常量。用这种办法来切换module为应用还是库。 

gradle.properties 文件中定义五个参数用来控制五个不同的模块是否是独立编译:

# isBuildModuleXX 用于控制是否单独编译,true:单独编译,false:library形式
#首页模块
isBuildModuleHome=true
#菜单模块
isBuildModuleMenu=true
#潮品模块
isBuildModuleFashions=true
#购物车模块
isBuildModuleShopping=true
#我的模块
isBuildModuleUser=true

在五个 module 的 build.gradle 中读取 gradle.properties中的常量,决定 module 为应用还是库:

if (isBuildModuleHome.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

3.2 不同的模块,确保 SDK 和引入库版本号一致

不同模块的依赖 SDK 版本不一致或者引入的库版本不一致会导致编译问题和兼容性问题。解决办法是在主项目最外层用一个文件来统一管理引入库和 SDK 的版本。

在工程的根目录创建文件 dependency.gradle:

ext {
    minSdkVersion = 16
    targetSdkVersion = 29
    compileSdkVersion = 29
    buildToolsVersion = "29.0.3"
    versionCode = 1
    versionName = "1.0"
    versions = [
            ...
            ...


    ]
    dependencies = [
        ...
        ...
    ]
}

在工程的 build.gradle 中引入文件 dependency.gradle:

apply from: "dependency.gradle"

在模块中的 build.gradle 实现对文件 dependency.gradle 的使用:

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

3.3 AndroidManifest 管理问题

在创建模块的时候,是以 application 的形式进行创建的,所以每个模块都有程序的启动入口。但是一旦整合起来后,如果继续保持多个启动入口,明显是有问题的。

在 main 目录下新增文件夹 compound,用于模块以 library 形式存在时候提供 AndroidManifest.xml ;而Android studio 产生的原路径下的 AndroidManifest.xml 文件保留,用于存储单独编译时候的 AndroidManifest.xml 文件。

 

android 模块化开发 android模块化和组件化_组件化_04

在 library 状态下的 AndroidManifest.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="zzw.cn.component.home">

    <application
        android:theme="@style/AppTheme">
        <activity android:name=".HomeActivity">
        </activity>
    </application>

</manifest>

单独编译状态下的 AndroidManifest.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="zzw.cn.component.home">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

不同状态下定义好了不同的 AndroidManifest.xml 文件了,而 AndroidStudio 该如何识别呢?

在组件的 build.gradle 文件的 android 中进行 manifest 的管理:

android {
    
    ...
    ...

    sourceSets {
        main {
            if (isBuildModuleHome.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }else{
                manifest.srcFile 'src/main/compound/AndroidManifest.xml'
            }
        }
    }
}

3.4 基础公共组件

以 library 形式创建模块 lib_base 和 lib_common,其中 lib_base用于第三方库的统一依赖,lib_common 用于封装公共部分,如对于网络请求等的二次封装等。

lib_base 的 build.gradle文件如下所示:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

def root_dependencies = rootProject.ext.dependencies

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api root_dependencies.kotlin
    api root_dependencies.appcompat
    api root_dependencies.androidx_core
    api root_dependencies.glide
    annotationProcessor root_dependencies.glide_compiler
    api root_dependencies.retrofit
    api root_dependencies.converter_gson
    api root_dependencies.retrofit_adapter_rxjava
    api root_dependencies.rxjava
    api root_dependencies.constraintlayout
    api root_dependencies.okhttp
    api root_dependencies.eventbus
    api root_dependencies.easypermissions
    api root_dependencies.coroutines_core

}

其中对于依赖的第三方库版本的定义在工程根目录下的文件 dependency.gradle 中:

ext {
    minSdkVersion = 16
    targetSdkVersion = 29
    compileSdkVersion = 29
    buildToolsVersion = "29.0.3"
    versionCode = 1
    versionName = "1.0"
    versions = [
            appcompat              : "1.1.0",
            androidx_core          : "1.2.0",
            kotlin                 : "1.3.70",
            glide                  : "4.11.0",
            glide_compiler         : "4.11.0",
            retrofit               : "2.7.2",
            converter_gson         : "2.7.2",
            retrofit_adapter_rxjava: "2.7.2",
            rxjava                 : "3.0.0",
            constraintlayout       : "1.1.3",
            okhttp                 : "4.0.0",
            eventbus               : "3.2.0",
            easypermissions        : "3.0.0",
            coroutines_core        : "1.3.5"


    ]
    dependencies = [
            kotlin                 : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin",
            androidx_core          : "androidx.core:core-ktx:$versions.androidx_core",
            appcompat              : "androidx.appcompat:appcompat:$versions.appcompat",
            glide                  : "com.github.bumptech.glide:glide:$versions.glide",
            glide_compiler         : "com.github.bumptech.glide:compiler:$versions.glide_compiler",
            retrofit               : "com.squareup.retrofit2:retrofit:$versions.retrofit",
            converter_gson         : "com.squareup.retrofit2:converter-gson:$versions.converter_gson",
            retrofit_adapter_rxjava: "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit_adapter_rxjava",
            rxjava                 : "io.reactivex.rxjava3:rxjava:$versions.rxjava",
            constraintlayout       : "androidx.constraintlayout:constraintlayout:$versions.constraintlayout",
            okhttp                 : "com.squareup.okhttp3:okhttp:$versions.okhttp",
            eventbus               : "org.greenrobot:eventbus:$versions.eventbus",
            easypermissions        : "pub.devrel:easypermissions:$versions.easypermissions",
            coroutines_core        : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines_core"
    ]
}

在 lib_commom 中定义对 lib_base的依赖,build.gradle 文件如下:

...
...

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api project(path: ':lib_base')
}

业务模块实现对 lib_commom 的依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':lib_common')
}

3.5 使用 ARouter 进行组件之间的调用和通信

由于业务组件间不存在依赖关系,不可以通过 Intent 进行跳转。组件化的目的是为了解决模块间的强依赖问题,这时候就需要借助于路由的,我使用的是阿里巴巴的开源框架 ARouter。

ARouter 的地址:https://github.com/alibaba/ARouter

3.5.1 ARouter 的导入

根据官方的文档,需要添加的依赖有:

dependencies {
    // Replace with the latest version
    compile 'com.alibaba:arouter-api:?'
    annotationProcessor 'com.alibaba:arouter-compiler:?'
    ...
}

在这里,我将 arouter-api 添加在 lib_base 中,arouter-compiler 必须添加在每个组件模块中。由于此项目采用的语言为 kotlin,配置如下:

需要在每个组件模块中添加:

android {
    
    ...
    ...

    defaultConfig {
        
        ...
        ...

        kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
    }

    ...
    ...

}

dependencies {

    ...
    ...

    kapt root_dependencies.arouter_compiler
    
    ...
    ...
}

而且还需要添加:

apply plugin: 'kotlin-kapt'

3.5.2 页面间的简单跳转

在目标页面 Activity/Fragment 类上写上 Route Path 的注解,这里的路径需要注意的是至少需要有两级,/xx/xx。

@Route(path = "/home/HomeActivity")
class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)
    }

    override fun onDestroy() {
        super.onDestroy()
    }
}

在需要进行跳转的页面进行触发跳转:

fun click(v: View) {
        when (v.id) {
            R.id.btn_home -> {
                ARouter.getInstance().build("/home/HomeActivity").navigation()
            }
            R.id.btn_menu -> {
                ARouter.getInstance().build("/menu/MenuActivity").navigation()
            }
            R.id.btn_fashions -> {
                ARouter.getInstance().build("/fashions/FashionsActivity").navigation()
            }
            R.id.btn_shopping -> {
                ARouter.getInstance().build("/shopping/ShoppingActivity").navigation()
            }
            R.id.btn_user -> {
                ARouter.getInstance().build("/user/UserActivity").navigation()
            }
        }
    }

效果如下:

android 模块化开发 android模块化和组件化_android_05

这里只是列举了页面的简单跳转,如果需要在跳转的时候携带参数等,可以参照 ARouter 的介绍:https://github.com/alibaba/ARouter

4.总结

此篇文章只是针对组件化进行简单的实现,其中还有很多问题没有描述具体的解决办法,比如如何在每个模块获取登录信息、如何避免资源名字冲突等问题。