Android使用Gradle插件统一组件化架构各组件的版本(上)

1,345 阅读6分钟

今天给大家介绍一种组件化架构,统一各个Module中的版本号、相似代码的、配置一种方案。 现在基本上中大型项目都是采用组件化架构,随着项目中的Library Module不断增多,问题也不断增多,下面来看看常见的一些问题

  1. 不同ModuleminSdk、targetSdk、versionCode、versionName统一版本号。
  2. 不同Module中的build.gradle有大量重复代码。
  3. dependencies三方库的版本不统一
  4. 向所有Module中的BuildConfig中添加静态常量麻烦
  5. aar和源码切换或者单Module运行增加编译速度

本文使用Gradle插件+build.gradle.kts ,前提需要有点基础,还在使用Groovy的可以先了解下kts

组件化架构

组件化架构、组件拆分、组件解耦、组件通信等等不是本文重点,下面引用京东的博客,不懂组件如何拆分的可以参考这个博客

image.png

新建一个工程

整体介绍一下项目工程

image.png

  1. app:App的宿主,可以理解就是一个壳,一般是Application和启动页
  2. layer_base:项目基础层,一般存放工具类等等
  3. layer_common:业务公共层,放一些多个project层共用的代码
  4. layer_project:业务层,一般就是具体的业务,比如,首页,搜索页,播放器等等
  5. layer_extends:业务扩展层,一般是跟项目无关的三方库,如ijk,友盟,等等
  6. version-plugin:是一个Gradle插件,用于统一项目配置,本文核心就是这里

插件

image.png

ProjectVersion

import java.util.Date

object ProjectVersion {

    const val compileSdk = 33
    const val targetSdk = 33
    const val minSdk = 19
    const val versionCode = 100
    const val versionName = "1.0.0"
    const val applicationId = "com.example.project"

    /**
     * 项目接口的地址
     */
    const val url = "http://baidu.com"

    /**
     * 定义参数
     */
    var param = false

    
    //可以根据项目需求定义更多的变量.......
    
    /**
     * 获取打包时间
     * @return String
     */
    fun getCurrDate(): String {
        val date = Date()
        val dateStr = date.time
        return "$dateStr"
    }
}

主要是一些build.gradle中涉及到的版本号,在这里统一定义。

Dependencies.kt

object Versions {
    const val kotlin = "1.8.0"
    const val lifecycle = "2.6.1"
    const val junit = "4.13.2"
    const val androidxJunit = "1.1.3"
    const val espresso = "3.4.0"
    const val kotlinxCoroutinesVersion = "1.6.4"
    const val roomVersion = "2.5.0"
    const val okhttpVersion = "3.12.0"
    const val routerVersion = "1.2.2"
    const val exoVersion = "1.1.1"
}

object Libraries {
    //Kotlin
    const val coreKtx = "androidx.core:core-ktx:${Versions.kotlin}"
    const val kotlinxCoroutinesAndroid =
        "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinxCoroutinesVersion}"
    const val kotlinxCoroutinesCore =
        "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinxCoroutinesVersion}"

    //Kotlin Ktx
    const val fragmentKtx = "androidx.fragment:fragment-ktx:1.6.0"
    const val activityKtx = "androidx.activity:activity-ktx:1.6.0"
    const val collectionKtx = "androidx.collection:collection-ktx:1.3.0"

    //AndroidX
    const val appcompat = "androidx.appcompat:appcompat:1.5.0"
    const val material = "com.google.android.material:material:1.9.0"
    const val annotation = "androidx.annotation:annotation:1.7.0"

   //省略代码。。。。。。。。。。。。。
   
}

主要是依赖三方库的地方,Versions可以统一版本号,

Extension.kt

import org.gradle.kotlin.dsl.DependencyHandlerScope


fun DependencyHandlerScope.kotlinCore() {
    "api"(Libraries.coreKtx)
    "api"(Libraries.kotlinxCoroutinesAndroid)
    "api"(Libraries.kotlinxCoroutinesCore)
}

fun DependencyHandlerScope.kotlinKtx() {
    "api"(Libraries.fragmentKtx)
    "api"(Libraries.activityKtx)
    "api"(Libraries.collectionKtx)
}

  //省略代码。。。。。。。。。。。。。

主要是为了将依赖库分组,比如kotlin相关的,可以用kotlinCore方法归类到一个分组,在Libray Module中引用的时候直接引用kotlinCore();如下面代码

源码位置

dependencies {
    //对应Extension.kt
    kotlinCore()
    lifecycle()
    kotlinKtx()
    commonView()
    androidTest()
    //当然也可以引入单个库
    api(Libraries.viewpager2)
}

这样就可以简化很多代码,为了防止组件之间相互引用,就需要实际项目经验来使用implementationapi了,当然这里也可以点进去(windows是ctrl+左键)

LibsBuildPlugin

package com.tools.plugin

import ProjectVersion
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

/**
 * @author: cb
 * @date: 2023/10/12
 * @desc: 统一多Module中Gradle的配置
 */
class LibsBuildPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            //配置library plugin,可以统一添加一些插件
            plugins.run {
                apply("com.android.library")
                apply("kotlin-android")
                apply("kotlin-parcelize")
            }
            //配置android{},注意这里是LibraryExtension
            extensions.configure<LibraryExtension> {
                compileSdk = ProjectVersion.compileSdk
                defaultConfig {
                    minSdk = ProjectVersion.minSdk
                    consumerProguardFiles("consumer-rules.pro")
                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                    commonDefaultConfig(this)//后面会讲
                }
                //Application和library相同的配置
                unifiedConfiguration(this)
            }
        }
    }
}

是一个Library ModuleGragle插件,拿到Project后可以调用其内部的方法,在这里可以解决问题1,2,3

AppBuildPlugin

package com.tools.plugin

import ProjectVersion
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

/**
 * @author: cb
 * @date: 2023/10/12
 * @desc: 统一多Module中Gradle的配置
 */
class AppBuildPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            //配置app plugin,可以统一添加一些插件
            plugins.run {
                apply("com.android.application")
                apply("kotlin-android")
                apply("kotlin-parcelize")
            }
            //配置android{},注意这里是ApplicationExtension
            extensions.configure<ApplicationExtension> {
                compileSdk = ProjectVersion.compileSdk
                defaultConfig {
                    minSdk = ProjectVersion.minSdk
                    targetSdk = ProjectVersion.targetSdk
                    versionCode = ProjectVersion.versionCode
                    versionName = ProjectVersion.versionName
                    vectorDrawables.useSupportLibrary = true
                    commonDefaultConfig(this)//后面会讲
                }
                lint { baseline = file("lint-baseline.xml") }
                //签名信息
                signingConfigs {
                    create("release") {
                        storeFile = file("../app/keystore.jks")
                        storePassword = "123456"
                        keyAlias = "key0"
                        keyPassword = "123456"
                    }
                }
                //Application和library相同的配置
                unifiedConfiguration(this)
            }
        }
    }
}

AppBuildPlugin和LibsBuildPlugin内容差不多,这里有差异的就是App对应LibraryExtensionlibrary对应ApplicationExtension,而两者相同的代码可以统一到一个方法中commonDefaultConfig(this)

签名配置不放这里也没关系,放到app build.gradle中也是可以的,看你的心情

PlugExt.kt

ApplicationExtension继承CommonExtension

LibraryExtension也是继承CommonExtension

所以在这里将AppLibrary中相同的代码统一处理

package com.tools.plugin

import ProjectVersion
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.DefaultConfig
import org.gradle.api.JavaVersion
import org.gradle.api.plugins.ExtensionAware
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions

/**
 * 统一app和lib中的公共配置项
 * @param ex CommonExtension<*, *, *, *>
 */
fun unifiedConfiguration(ex: CommonExtension<*, *, *, *>) {
    ex.buildTypes {
        getByName("release") {
            isMinifyEnabled = true
        }
        getByName("debug") {
            isMinifyEnabled = false
        }
    }
    ex.compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    ex.viewBinding {
        enable = true
    }
    ex.kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
    ex.packagingOptions {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

/**
 * buildConfigField 可以在BuildConfig中定义一些静态常量,所有的module中都有
 * @param config DefaultConfig
 */
fun commonDefaultConfig(config: DefaultConfig) {
    //接口地址
    config.buildConfigField("String", "BASE_URL", ""${ProjectVersion.url}"")
    //添加参数到所以module的BuildConfig
    config.buildConfigField("boolean", "PARAM", "${ProjectVersion.param}")
    //可以任意定义.....
}

fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
    (this as ExtensionAware).extensions.configure("kotlinOptions", block)
}

BuildConfig中定义一些静态常量这个需求在我们项目中经常会用到,如果项目Module有很多,不可能每一个module都去写一个,所以这里统一在这里处理了解决了问题4,当然之前使用Groovy时,使用ext{}也是可以实现的。

其他的没什么说的,就是一些经常用到的东西,如viewBinding,混淆配置等等,CommonExtension中还有很多配置可以根据自己的项目需求定义,这里不在多介绍了。

配置插件

插件里的settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "version-plugin"
include (":version-plugin")
插件里的build.gradle.kts
plugins {
    `kotlin-dsl`
}
gradlePlugin {
    //定义gradle插件
    plugins{
        register("AppGradlePlugin") {
            //app插件的id
            id = "example.app.plugin"
            //插件完整的包名类名
            implementationClass = "com.tools.plugin.AppBuildPlugin"
        }
        register("LibsBuildGradlePlugin") {
            //library插件的id
            id = "jx.libs"
            //插件完整的包名类名
            implementationClass = "com.tools.plugin.LibsBuildPlugin"
        }
    }
}

dependencies {
    //引入gradle
    implementation("com.android.tools.build:gradle:7.4.2")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}

在这里定义了两个插件App插件和Library插件

注意:定义插件的完整的包名类名不能错需要匹配

image.png

引入version-plugin

在项目的settings.gradle.kts中引入

pluginManagement {
    includeBuild("version-plugin")
   //省略代码。。。。。。。
}

至此插件代码已经ok了

在Library中使用插件

plugins {
    //引入我们自己定义的Library插件
    id("jx.libs")
}

android {
    namespace = "com.example.util"

    defaultConfig {
    }
}

dependencies {
    //对应Extension.kt
    kotlinCore()
    lifecycle()
    kotlinKtx()
    commonView()
    androidTest()
}

代码是不是很简洁,如需改动配置并应用到所有Library Module直接修改Library插件就可以了

在App中使用插件

plugins {
    //对应com.tools.plugin.AppBuildPlugin
    id("example.app.plugin")
    id("com.google.devtools.ksp")
    id("therouter")
}

android {
    namespace = ProjectVersion.applicationId

    defaultConfig {
        applicationId = ProjectVersion.applicationId
        ndk {
            abiFilters.add("armeabi-v7a")
        }
    }

    buildTypes {
        //可以重写,覆盖插件中定义的
        getByName("debug") {
            isDebuggable = true
            isMinifyEnabled = false
        }
        getByName("release") {
            isDebuggable = false
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            signingConfig = signingConfigs.getByName("release")
        }
    }
  //省略代码。。。。。。
}

dependencies {
    router()
    implementation(project(":common_core"))
    implementation(project(":proj_home"))
    implementation(project(":proj_search"))
}

如果插件和build.gradle.kts有相同配置,会覆盖插件中的公共配置

至此本文所有核心内容都已经讲完了我们编译运行一下

随便找个module看下BASE_URLPARAM都已经添加进来了

image.png

项目源码

image.png

我这里简单简单创建了几个Module演示组件化架构,需要的小伙伴可以clone源码参考,路由框架使用的是货拉拉Therouter

ARouter不支持AGP8.0并且长时间不维护了

问题5

2024年8月29日10:29:11

问题5已解决,详细请看这篇文章

缺点或者使用中遇到的一些问题

  1. 修改插件代码后Android Studio不会提示需要同步,需要我们自己去同步一次
  2. 修改基础层Module时编译耗时
  3. 后续补充。。。。。。。。。。。。。。。

总结

本文提供一种思路,用Gradle插件来统一版本号和公共的一些配置

如需在项目中使用要全面了解后再使用,需要权衡各个方面,就比如Kts语法和Groovy语法之间的差异、Gradle版本之间的差异等等都有可能导致项目报错运行不起来。

老项目建议不要轻易尝试,新项目可以使用起来,