Android组件化开发

385 阅读8分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

为什么使用组件化

  • 单工程项目问题

    • 随着业务迭代代码越来越臃肿。
    • 代码碎片化验证,出现bug,难以很快定位问题,维护困难。
    • 对于新功能的开发,避免改动影响原有业务,导致开发成本不断增加。
    • 项目编译时间长,中大型项目编译时间动辄10几分钟,开发效率低。
    • 代码复用性差,功能代码耦合了大量业务代码,难以拆分复用。
    • 不易于团队开发,功能耦合严重,边界不明确,代码合并经常发生冲突。
  • 组件化优势

    • 模块解耦,对各业务、功能模块进行分离,每个模块都是一个独立的组件,可独立运行(业务模块)。
    • 代码复用,模块解耦之后也就意味着高度可复用性。
    • 加快编译速度,相对于单工程项目,组件化每个业务模块可独立编译运行,大大加快了编译速度。
    • 易于团队合作开发,各个组件指定人员负责开发,各组件之间没有耦合,功能边界清晰,可以有效降低沟通成本。

模块化、组件化和插件化及差别。

  • 模块化,更倾向于对独立的功能进行模块化封装,如:网络访问、图片加载和log库等,但对于业务模块的解耦不彻底,依旧容易出现各业务模块高度耦合的问题。
  • 组件化,各功能模块、业务模块高度解耦互不影响,各业务模块之间没有直接相互引用,之间数据交互通过路由实现。
  • 插件化,组件化注重编译时的动态配置项目,插件化注重运行时动态配置。组件化编译安装后功能确定,不可动态变更,插件化编译安装后,可以动态加载功能模块。可以说组件化是插件化的基础,插件化是通过一些动态加载机制,使得各组件代码(通常是aar或dex)可以在apk被安装运行后动态进行加载。

组件化开发过程

项目组件及层级划分

组件划分是组件化开发中最重要的一步,通常会按照功能和业务将项目进行分层设计。如下图:

  • library层

    提供通用功能,如网络、图片加载和log库等,通常是基于第三方开源方案做的二次封装库,因为修改频率低也可以作为aar或远程仓库引用。

  • common层

    提供基类的封装BaseActivity和BaseFragment等,并且引用所有的功能组件,为上层业务组件提供支持,而且同一维护所以基础组件有助于基本组件的版本同一。通常也将所以第三方库的引用放到此组件中,同一管理维护。

  • component层

    业务组件层,将业务模块进行组件化拆分,隔离成功能完整的独立组件,各业务组件间禁止直接相互引用,各组件可配置独立调试、运行和测试。如:登录组件、注册组件、设置组件和用户中心组件等。

  • app壳

    承载app的启动入口Activity和全局的Application等,没有任何复杂的业务逻辑代码,是引用各业务组件的“壳工程”。

各层级组件只能向下依赖,同层之间尽量避免直接引用依赖,越下层组件的功能越稳定,修改频率越小。

组件通信路由框架实现

上面提到了业务组件之间是项目隔离完全解耦的,那么组件之间页面的跳转,数据的传递,甚至服务的调用就变成了首要需要解决的问题,这就引入了路由的概念。

路由routing)就是通过互联的网络信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即[网络层])。

这是网络中路由概念的解释,在组件化开发中“路由”的概念与之类似,核心概念就是将目标页面与其对象解绑,传统的Android路由是通过Intent实现,Intent要求必须能够直接引用到目标页面类或者其Action,而路由是通过映射关系将目标页面与其“路由路径”相绑定,并维护统一的路由表,当发生页面跳转时,会从路由表中根据路径取出相应的目标页面对象进行跳转处理。(类似隐式Action跳转,但是相对于隐式跳转,不需要在Manifest中声明,可以统一管理路由表,更易于维护,且可以支持动态添加路由)

比较著名的路由框架有

当然了处理页面Activity跳转,还有页面间数据传递,Fragment对象的获取,组件开放服务的调用等,这些上面的路由框架中都有提供相应的解决方案。

Gradle支持

组件化开发最大的特点就是各业务组件是可以独立运行调试的,但是在打包发布时又需要将所有的业务组件组装成一个app进行发布,为了实现这个功能就需要在Gradle进行相应的配置,来满足需求。

我们知道Android Studio是使用Gradle进行构建的,而Android Gradle插件提供了2种插件类型:application和library,对应的构建输出分别是apk和aar。在单组件调试时,业务组件是以application形式存在的,而在打包发布时业务组件是以library形式存在的。当然我们可以在调试和发布时手动修改组件的apply plugin,但是这样也太傻,太繁琐了。为了尽量简化这个过程,我们需要统一进行配置:

简单的组件化项目开发配置

  • 在gradle.propertites配置统一变量

    在项目根目录的gradle.propertites文件中定义的变量可以被项目中所有build.gradle访问。所以我们在文件中添加添加isModule变量,true即为各组件独立调试模式,false是整合发布模式。

    android.useAndroidX=true
    # Automatically convert third-party libraries to use AndroidX
    android.enableJetifier=true
    
    # 模块是否独立运行开发
    isModule=false
    
  • 修改build.gradle

    之后我们就可以在业务组件的build.gradle中添加:

    if (isModule.toBoolean()){
        apply plugin: 'com.android.application'
    }else {
        apply plugin: 'com.android.library'
    }
    
    android{
      //省略
    }
    
  • 修改AndroidManifest

    业务组件想要独立运行调试,除了需要将插件修改为com.android.library外,还需要做的一件事是配置AndroidManifest.xml添加<application>节点,声明启动activity等,所以就需要两个AndroidManifest.xml文件来进行切换,需要通过gradle的sourceSets 来根据不同模式加载不同的AndroidManifest文件。

    android{
      	//省略
      
        sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                }
            }
        }
    }
    

    然后在项目的src/main目录下新建文件夹module添加AndroidManifest.xml并正常添加组件运行需要的权限、和声明activity,并设置Activity的程序入口属性。

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.zhong.demo">
    
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
        <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/Theme.GradleDemo">
            <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>
    

    而在组件正常的AndroidManifest.xml中只需要声明运行权限并声明Activity(不需要设置app入口属性)。

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.zhong.demo">
    
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
        <application>
            <activity android:name=".MainActivity"/>
        </application>
    
    </manifest>
    

到这里最简单的组件化开发项目配置就基本完成了,就可以进入正式项目开发了。

Gradle配置统一管理

通过上面的步骤已经可以进行简单的组件化开发了,但是过于简单的配置依然可能存在一些问题:

  • 资源文件冲突,各组件独立开发不可避免的会出现资源文件命名冲突问题,就到导致最终整合发布时的各种异常。
  • 依赖库版本管理,组件独立开发可能出现资源库引用版本不一致的问题。
  • 各组件相同的配置,需要配置多次。比如路由框架我们选用了阿里的ARouter,那么就需要在每一个组件的gradle文件中添加库应用并声明编译配置属性,未免太繁琐。

config.gradle

在项目根目录添加config.gradle用于统一管理编译环境和第三方库版本。

import java.text.SimpleDateFormat

ext {
    //build
    android = [
            compileSdkVersion: 30,
            buildToolsVersion: '30.0.2',
            minSdkVersion    : 19,
            targetSdkVersion : 26,
            applicationId    : 'com.xxx.xxxx',
            versionCode      : 1,
            versionName      : "V1.0.0_1_${releaseTime()}"
    ]

    //AndroidX
    androidx = [
            appcompat       : 'androidx.appcompat:appcompat:1.1.0',
            material        : 'com.google.android.material:material:1.1.0',
            constraintlayout: 'androidx.constraintlayout:constraintlayout:1.1.3',
            multidex        : 'androidx.multidex:multidex:2.0.1',
            leanbackX       : 'androidx.leanback:leanback:1.0.0'
    ]


    //dependencies
    dependencies = [
            //gson
            gson_version                : '2.8.5',
            //butterknife
            butterknife_compiler_version: '10.2.2',
            butterknife_version         : '10.2.3',
            //glide
            glide_version               : '4.10.0',
            //retrofit
            retrofit_version            : '2.3.0',
            converter_gson_version      : '2.3.0',
            //OkHttp
            okhttp_version              : '3.8.0',
            logging_interceptor_version : '3.8.0',
            //mmkv
            mmkv_version                : '1.2.11',
            //ARouter
            arouter_api_version         : '1.5.2',
            arouter_compiler_version    : '1.5.2'
    ]
}

static def releaseTime() {
    return new SimpleDateFormat("yyyyMMddHH").format(new Date())
}

然后在项目根目录的build.gradle中添加:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"

buildscript {
  //省略
}

就可以在所有的gradle文件中使用了:

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

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

        //省略
    }
}

common.gradle

在根目录添加common.gradle用于统一维护所以组件配置项,同样需要先在gradle.properties中添加变量isModule=false

然后在common.gradle针对组件的几种类型:application、library和可动态切换的组件,分别进行配置并对通用的配置项统一处理。

project.ext{
    //设置app配置
    setApplicationConfig = {
        extension ->
            //指定为application,代表该模块可以单独调试
            extension.apply plugin: 'com.android.application'
            extension.description "app"

            //设置项目的android
            setAppAndroidConfig extension.android
            //设置项目的三方库依赖
            setDependencies extension.dependencies

    }

    //设置library配置(只可以作为lib,不可单独调试)
    setLibraryConfig = {
        extension ->
            //library,代表只是单纯的库,不需要依赖其他模块
            extension.apply plugin: 'com.android.library'
            extension.description "lib"

            setLibAndroidConfig extension.android
            setDependencies extension.dependencies
    }

    //设置动态切换组件,可单独调试的业务组件
    setComponentConfig = {
        extension ->
            if (isModule.toBoolean()) {
                extension.apply plugin: 'com.android.application'
                extension.description "app"
            } else {
                extension.apply plugin: 'com.android.library'
                extension.description "lib"

            }

            //设置通用Android配置
            setComponentAndroidConfig extension.android
            //设置通用依赖配置
            setDependencies extension.dependencies
    }

    //设置application的android配置
    setAppAndroidConfig = {
        extension -> //extension 相当于 android 对象
            extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
            extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
            extension.defaultConfig {
                applicationId rootProject.ext.android.applicationId
                minSdkVersion rootProject.ext.android.minSdkVersion
                targetSdkVersion rootProject.ext.android.targetSdkVersion
                versionCode rootProject.ext.android.versionCode
                versionName rootProject.ext.android.versionName

                multiDexEnabled true

                multiDexKeepProguard file("${rootProject.rootDir}/multiDexKeep.pro") // keep specific classes using proguard syntax
                multiDexKeepFile file("${rootProject.rootDir}/mainDexList.txt") // keep specific classes
                extension.flavorDimensions "versionCode"

                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 编译生成路由
                javaCompileOptions {
                    annotationProcessorOptions {
                        arguments = [AROUTER_MODULE_NAME: project.getName()]
                    }
                }
            }

            //使用的jdk版本
            extension.compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }

            extension.buildFeatures {
                dataBinding = true
                viewBinding = true
            }
    }

    //设置library公共的android配置
    setLibAndroidConfig = {
        extension -> //extension 相当于 android 对象
            extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
            extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
            extension.defaultConfig {
                minSdkVersion rootProject.ext.android.minSdkVersion
                targetSdkVersion rootProject.ext.android.targetSdkVersion
                versionCode rootProject.ext.android.versionCode
                versionName rootProject.ext.android.versionName

                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 编译生成路由
                javaCompileOptions {
                    annotationProcessorOptions {
                        arguments = [AROUTER_MODULE_NAME: project.getName()]
                    }
                }
            }
            extension.compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }

            extension.buildFeatures {
                dataBinding = true
            }
    }

    //设置业务组件的android配置
    setComponentAndroidConfig = {
        extension -> //extension 相当于 android 对象
            extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
            extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
            extension.defaultConfig {
                minSdkVersion rootProject.ext.android.minSdkVersion
                targetSdkVersion rootProject.ext.android.targetSdkVersion
                versionCode rootProject.ext.android.versionCode
                versionName rootProject.ext.android.versionName
                multiDexEnabled true
                extension.flavorDimensions "versionCode"

                multiDexKeepProguard file("${rootProject.rootDir}/multiDexKeep.pro") // keep specific classes using proguard syntax
                multiDexKeepFile file("${rootProject.rootDir}/mainDexList.txt") // keep specific classes
                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 编译生成路由
                javaCompileOptions {
                    annotationProcessorOptions {
                        arguments = [AROUTER_MODULE_NAME: project.getName()]
                    }
                }
            }
            extension.buildFeatures {
                //启用自动绑定view id
                dataBinding = true
                viewBinding = true
            }

            //使用的jdk版本
            extension.compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }

            //动态改变清单文件资源指向
            extension.sourceSets {
                main {
                    if (isModule.toBoolean()) {
                        manifest.srcFile 'src/main/module/AndroidManifest.xml'
                    } else {
                        manifest.srcFile 'src/main/AndroidManifest.xml'
                    }
                }
            }
    }


    //公用的三方库依赖,慎重引入,主要引入基础库依赖
    setDependencies = {
        extension ->
            extension.implementation fileTree(dir: 'libs', include: ['*.jar'])

            //ARouter 路由apt插件,用于生成相应代码,每个module都需要
            extension.annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
            extension.implementation 'com.alibaba:arouter-api:1.5.0'
            extension.implementation 'com.alibaba:arouter-compiler:1.2.2'

            //各组件公共的引用库,通常是android系统库,第三方库不建议添加此处
            extension.implementation rootProject.ext.androidx.appcompat
            extension.implementation rootProject.ext.androidx.material
            extension.implementation rootProject.ext.androidx.constraintlayout
            extension.implementation rootProject.ext.androidx.multidex
    }

}

看着好像很复杂的样子,其实就是根据组件类型(application、library和可动态切换的组件),分成两组定义了plugin和androidConfig,除此之外还定义公共的dependencies。

解决资源文件冲突

组件化开发中,因为不同人员负责不同组件的开发,不可免的会出现资源文件重名问题,在最终整合编译时就可能出现资源文件冲突问题,为了解决这个问题可以在gradle文件中,使用resourcePrefix在编译时进行资源前缀检查,强制要求各组件资源文件按照固定格式命名,避免资源文件冲突问题。

//在各组件的build.gradle中
android {
		//...
    //资源前缀强制命名
    resourcePrefix "signup_"
  	//或者直接用module命名限制
  	//resourcePrefix "${project.name}_"
  	//...
}

之后module中的所有资源文件在编译时都会进行检查,资源文件前缀是否是signup_,如果不是则会报错提示。

最终的项目结构如下:

总结

至此万里长征的第一步算是迈出去了,之后就可以步入正式开发阶段了。

参考博客: