【Android】组件化搭建的一般流程

文章目录

  • [1. 组件化概述](#1. 组件化概述)
  • [2. 组件化的一般结构](#2. 组件化的一般结构)
  • [3. 组件化的流程](#3. 组件化的流程)
    • [3.1 组件模式和集成模式的转换](#3.1 组件模式和集成模式的转换)
    • [3.2 统一管理项目依赖库](#3.2 统一管理项目依赖库)
      • [3.2.1 Gradle Version Catalog(TOML 方式)](#3.2.1 Gradle Version Catalog(TOML 方式))
      • [3.2.2 config.gradle(传统 Groovy 方式)](#3.2.2 config.gradle(传统 Groovy 方式))
    • [3.3 组件之间AndroidManifest合并问题](#3.3 组件之间AndroidManifest合并问题)
    • [3.4 Application的获取](#3.4 Application的获取)
    • [3.5 组件之间调用和通信](#3.5 组件之间调用和通信)
    • [3.6 组件之间资源名冲突](#3.6 组件之间资源名冲突)
  • [4. 总结](#4. 总结)

1. 组件化概述

如果一个APP所有的代码和界面都写在同一个模块,即app模块,那么就会造成很多的问题:整个模块变得非常庞大,编译时电脑会非常卡,代码之间耦合严重等。而且如果涉及到多人协作开发,那么每个人都要熟悉如此之多的代码,增加项目的维护成本。又因为单一工程下的代码耦合严重,每修改一处代码都要重新编译打包运行,导致非常耗时,所以就有了模块化和组件化的概念。

模块:将一个程序按照其功能做拆分,分成相互独立的模块,以便每个模块只包含与其功能相关的内容,比如登录模块、首页模块等等。

组件:组件指的是单一的功能组件,如登录组件、视频组件、支付组件等,每个组件都可以以一个单独的module开发,并且可以单独抽出来作为SDK对外发布使用。可以说往往一个模块包含了一个或多个组件。

那么组件化有什么优势呢?

组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合

  1. 提升编译与构建速度:只需编译当前修改的模块,同时Gradle支持多模块并行编译,进一步缩短了构建时间。

  2. 降低代码耦合,提升可维护性:模块间通过接口、路由或事件通信、而不是直接依赖实现类,代码结构清晰,职责单一,便于理解和重构。

  3. 提升开发效率:不同业务模块由不同成员负责,互不干扰。各模块可独立开发、测试,减少Git冲突。

  4. 增强功能复用能力:类似引用的第三方库,可以将基础组件或功能剥离。在新项目中微调或直接使用。

2. 组件化的一般结构

组件化工程模型,

组件间的依赖关系是上层依赖下层,修改频率是上层高于下层。

  • APP壳工程 :项目的"空壳"主模块,在这个模块中,不包含任何具体业务逻辑 ,但是要依赖所有需要集成的业务组件,并且声明全局Application。简单来说,它是最终APK的"容器",负责组装各组件。

  • 集成模式:所有业务组件以library(库)形式被"app壳工程"依赖,最终合并打包成一个完整的APK,整个App是一个统一的整体,各业务组件不能独立运行。

  • 组件模式 :每个业务组件可作为独立的Application运行,拥有自己的入口(Launcher Activity)和Application类,通常通过gradle.properties中的开关(如isModule=false)控制。支持单独开发、调试、测试某个功能模块。因为无需构建整个项目,编译速度极快。

  • 业务组件:按业务功能划分的独立模块,如"商品详情","订单管理"等。在组件模式下是完整App(有Launcher Activity、Application),在集成模式下是library,通过路由(ARouter)与其他组件通信。

  • 功能组件:提供通用技术能力的模块,与具体业务无关。如网络请求封装(Retrofit + OkHttp),图片加载(Glide)等。注:功能组件始终作为library,不可独立运行。

  • Main组件:一种特殊的业务组件,包含App的启动页(Splash)、登录页、主界面。

  • Common组件:一种特殊的功能组件,是整个项目的地基。主要作用有:统一管理第三方库依赖,声明App需要的权限,封装通用工具类,存放全局资源等

解释完上面一些基本的概念,再来看看这张组件化工程模型图。由图可知,业务组件之间是独立的,并没有相互依赖关系,这些业务组件在集成模式下是一个个library,被app壳工程所依赖,组成一个具有完整功能的App。但是在组件模式下,业务组件又变成了一个个Application,它们可以独立开发调试,由于在组件模式下,业务组件的代码量向比于完整项目差了很远,因此在编译运行时可以显著减少时间。

解决了组件分层问题,现在又有新的问题产生了:由于各个业务模块相互独立,那么怎么实现一个模块中的某个页面跳转到另一个模块呢?也就是组件之间是如何进行通信的?

这张图展示了组件化工程模型下的业务关系,由于业务之间不再直接引用和依赖,传统方式无法实现组件之间的通信。而是引入了"路由"的概念,通过"路由"这样一个中转站间接产生联系。

3. 组件化的流程

3.1 组件模式和集成模式的转换

Android项目中Module主要有两种属性,分别是:

  • application属性,可以单独调试的Android程序:

    groovy 复制代码
    apply plugin: 'com.android.application'
  • library属性,不能独立运行,一般是Android程序依赖的库文件:

    groovy 复制代码
    apply plugin: 'com.android.library'

每个模块的build.gradle下都应定义这两个属性,根据一个总的开关来判断此时应该是组件模式还是集成模式。这个总开关定义在gradle.properties(Android项目的根目录下),这个文件的一个重要的属性:在Android项目中任何一个build.gradle文件中都可以把gradle.properties中的常量读出来。

gradle.properties中定义一个常量值表示是否是组件模式开发(true表示是,false表示否):

groovy 复制代码
# 每次修改后,记得点击"Sync Project"同步
isModule=false

然后在业务组件的build.gradle中读取isModule总开关,但是由于gradle.properties中的数据类型都是String类型,而我们需要的是Boolean值,所以需要转换一下:

groovy 复制代码
if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

3.2 统一管理项目依赖库

为了方便我们统一管理第三方库,提供统一的依赖第三方库的入口,主要有两种主流方式,分别为Gradle Version Catalog(TOML 方式)config.gradle(传统 Groovy 方式) 。第一种方式建议用于新项目或已升级构建环境的项目,传统Groovy方式兼容旧项目,无需升级Gradle,适合Java/Groovy项目

3.2.1 Gradle Version Catalog(TOML 方式)

在项目根目录下创建gradle/lib.versions.toml

toml 复制代码
[versions]
# 所有版本号集中定义在这里,使用语义化命名
agp = "8.13.2"               # Android Gradle Plugin
compileSdk = "36"
minSdk = "21"
targetSdk = "36"

# 第三方库版本
retrofit = "2.11.0"
glide = "4.16.0"
arouter = "1.5.2"
junit = "5.10.0"
androidx-core = "1.12.0"
okhttp = "4.12.0"

[libraries]
# 格式:alias = { group = "...", name = "...", version.ref = "..." }
# 或直接写 version(不推荐,不利于统一管理)

# AndroidX
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-core" }

# 网络
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }

# 图片加载
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }

# 路由
arouter-api = { group = "com.alibaba", name = "arouter-api", version.ref = "arouter" }
arouter-compiler = { group = "com.alibaba", name = "arouter-compiler", version.ref = "arouter" }

# 测试
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }

[plugins]
# 插件别名(用于 plugins {} 块)
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

分析:

  • [versions]下只定义版本字符串,比如retrofit = "2.11.0",方便统一管理版本号

  • [libraries]下定义第三方库别名,一般格式:alias = { group = "...", name = "...", version.ref = "..." }alias是在代码中引用该依赖时所使用的别名,比如传统引入依赖:com.squareup.retrofit2:retrofit:2.11.0在这里就可以这样写:retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }version.ref就是上面在[version]中定义的版本号。

  • [plugins]中定义插件别名,基本思想与[libraries]一致

关于引入依赖

Groovy DSL(build.gradle)中:

groovy 复制代码
implementation libs.retrofit

Kotlin DSL(build.gradle.kts)中:

kotlin 复制代码
implementation(libs.retrofit)

3.2.2 config.gradle(传统 Groovy 方式)

  1. 在项目根目录下创建config.gradle

    groovy 复制代码
    ext { // Gradle中的一个扩展快,允许在项目范围内定义额外的属性或对象
        // Android SDK 配置
        android = [
            compileSdkVersion: 36,
            minSdkVersion    : 21,
            targetSdkVersion : 34,
            versionCode      : 100,
            versionName      : "1.0.0"
        ]
    
        // 所有第三方库的版本号集中定义
        versions = [
            appcompat       : "1.6.1",
            constraintlayout: "2.1.4",
            junit           : "4.13.2",
            junit5          : "5.10.0",
            mockito         : "5.7.0",
            retrofit        : "2.11.0",
            gson            : "2.10.1",
            okhttp          : "4.12.0",
            glide           : "4.16.0",
            arouter         : "1.5.2"
        ]
    
        // 依赖库完整坐标(group:name:version)
        // 使用versions中定义的版本号来构建完整的依赖字符串
        deps = [
            // AndroidX
            appcompat        : "androidx.appcompat:appcompat:${versions.appcompat}",
            constraintlayout : "androidx.constraintlayout:constraintlayout:${versions.constraintlayout}",
    
            // Test
            junit4           : "junit:junit:${versions.junit}",
            junit5           : "org.junit.jupiter:junit-jupiter:${versions.junit5}",
            mockito          : "org.mockito:mockito-core:${versions.mockito}",
    
            // Network
            retrofit         : "com.squareup.retrofit2:retrofit:${versions.retrofit}",
            retrofitGson     : "com.squareup.retrofit2:converter-gson:${versions.retrofit}",
            okhttpLogging    : "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}",
    
            // Image
            glide            : "com.github.bumptech.glide:glide:${versions.glide}",
    
            // Router
            arouterApi       : "com.alibaba:arouter-api:${versions.arouter}",
            arouterCompiler  : "com.alibaba:arouter-compiler:${versions.arouter}"
        ]
    }

    可以利用Groovy中的字符串插值${}避免重复写版本号,这样后续如果要修改版本号,只用修改一行。

  2. 构建好config.gradle后,在settings.gradle中引入该配置:

    groovy 复制代码
    // 应用 config.gradle 脚本
    apply from: 'config.gradle'

    这样ext中定义的内容就会成为rootProject的扩展属性,所有子模块都能访问

  3. 在各个模块的build.gradle中使用:

    groovy 复制代码
    // app/build.gradle
    android {
        compileSdk rootProject.ext.android.compileSdkVersion
    
        defaultConfig {
            minSdk rootProject.ext.android.minSdkVersion
            targetSdk rootProject.ext.android.targetSdkVersion
            versionCode rootProject.ext.android.versionCode
            versionName rootProject.ext.android.versionName
        }
    }
    
    dependencies {
        implementation rootProject.ext.deps.appcompat
        implementation rootProject.ext.deps.retrofit
        implementation rootProject.ext.deps.retrofitGson
        implementation rootProject.ext.deps.glide
    
        annotationProcessor rootProject.ext.deps.arouterCompiler
    
        testImplementation rootProject.ext.deps.junit4
        androidTestImplementation rootProject.ext.deps.junit5
    }

3.3 组件之间AndroidManifest合并问题

在组件化开发中,当项目处于组件模式时,每个业务模块需要一个完整的AndroidManifest.xml,包含<application>和启动Activity(带LAUNCHER),才能作为独立App运行。在集成模式时,最终APK只能有一个主AndroidManifest.xml(来自App壳工程),其他模块的AndroidManifest.xml需要被合并进去,且不能包含冲突的内容(如多个LAUNCHER,重复的Application等)。

通用做法是使用多套Manifest文件,通过Gradle配置动态切换,核心思想就是为每个业务模块准备两套AndroidManifest.xml,一套用于独立运行,一套用于集成。通过Gradle动态指定使用哪一套。

比如,我现在有两个模块:

分别是commonmodule_user两个模块,在module_user下创建manifest文件夹,这个清单文件是模块单独运行时的清单文件,包含Application和启动页

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name="debug.UserDebugApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ARouter">
        <activity
            android:name=".LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

当前模块只有一个LoginActivityApplication是在当前模块包下创建的UserDebugApplication

java 复制代码
// debug/UserDebugApplication
package debug;

import com.example.common.BaseApplication;

public class UserDebugApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}

只用继承BaseApplication,重写onCreate方法,作为module_user模块独立运行时的Application。关于全局Context的管理,会在下一个小节详细介绍。

而main模块下的AndroidManifest只注册当前模块的四大组件:

xml 复制代码
<!--src/main/AndroidManifest.xml-->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.module_user">

    <application>
        <activity
            android:name=".LoginActivity"
            android:exported="false" />
    </application>

</manifest>

这样,就有了不同模式下的AndroidManifest文件,使用Gradle动态配置清单文件:

groovy 复制代码
// ...
android {
    // ...
	sourceSets {
        main {
            manifest.srcFile isModule ?
                    'src/main/AndroidManifest.xml' :
                    'src/main/manifest/AndroidManifest.xml'
        }
    }
    // ...
}
// ...

isModule为true,表示当前处于组件模式开发,在build.gradle中指定清单文件为src/main/AndroidManifest.xml,编译器会自动合并到app模块下的清单文件中。否则作为一个独立的APK运行,使用src/main/manifest/AndroidManifest.xml

3.4 Application的获取

在组件化开发中,每个模块(module)可能是一个独立的library或Application。不同模块独立运行时要正确获取到主App的Application实例,才能正确被打包编译。另外,在自己组件中开发时需要获取全局的Context ,这时就可以在common模块中引入BaseApplication,所有业务组件和主App的Application都继承自common模块中的BaseApplication。这样无论在哪种模式下,只要最终的Application继承了BaseApplication,就能通过它安全地获取到全局Context。

那么怎样控制集成模式下模块自己的Application不会和主App地Application冲突呢?这主要是利用Gradle地sourceSetsisModule标志,在不同模式下加载不同的AndroidManifest.xml和Java代码(排除debug/**包)

  1. 在common模块中新建BaseApplication,提供全局单例,数据初始化等操作:

    java 复制代码
    package com.example.common;
    
    import android.app.Application;
    import android.content.Context;
    
    import com.alibaba.android.arouter.launcher.ARouter;
    
    public class BaseApplication extends Application {
        private static BaseApplication instance;
        @Override
        public void onCreate() {
            super.onCreate();
            instance = this;
            
            // 直接开启日志和调试模式,不依赖 BuildConfig.DEBUG(library的DEBUG恒为false)
            ARouter.openLog();
            ARouter.openDebug();
            
            // 初始化ARouter
            ARouter.init(this);
        }
    
        // 获取全局Context
        public static Context getAppContext() {
            return instance.getApplicationContext();
        }
    }

    注意,这里ARouter的初始化不要对BuildConfig.DEBUG进行判断,因为library的DEBUG恒为false,否则模块在单独调试时ARouter初始化不成功,会导致应用崩溃。

  2. 在业务组件(module_user)中新建实例Application继承common中的BaseApplication

    java 复制代码
    package debug;
    
    import com.example.common.BaseApplication;
    
    public class UserDebugApplication extends BaseApplication {
        @Override
        public void onCreate() {
            super.onCreate();
        }
    }
  3. 在模块单独调试的Manifest中指定Application为我们自己新建的Application:

    xml 复制代码
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
        <application
            android:name="debug.UserDebugApplication"
            ...>
            ...
        </application>
    </manifest>
  4. 在业务模块中的build.gradle中使用sourceSetsisApplication指定两种模式下的AndroidManifest.xml

    groovy 复制代码
    android {
    	sourceSets {
            main {
                manifest.srcFile isApplication ?
                        'src/main/manifest/AndroidManifest.xml' :
                        'src/main/AndroidManifest.xml'
    			
                // 在集成模式下排除debug文件夹
                if (!isApplication) {
                    java.exclude 'debug/**'
                }
            }
        }
    }

3.5 组件之间调用和通信

在组件化开发时,组件之间时没有依赖关系的,我们无法使用显示调用来跳转页面,因为组件化的目的之一就是解决组件之间的强依赖关系。那么怎么实现组件之间的调用和通信呢,这个时候就需要引入"路由"的概念了,在第一节提到过,路由就是起到一个转发的作用。

主流做法是阿里巴巴开源的ARouter路由框架,用于组件化架构下的模块间通信。它通过注解 + APTAnnotation Processing Tool)在编译期自动生成路由映射表,避免了反射带来的性能损耗。

一般步骤:

  1. 添加依赖,首先在lib.versions.toml中添加版本号和libraries

    toml 复制代码
    [versions]
    // ...
    arouter-api = "1.5.2"
    arouter-compiler = "1.5.2"
    // ...
    [libraries]
    arouter-api = { group = "com.alibaba", name = "arouter-api", version.ref = "arouter-api" }
    arouter-compiler = { group = "com.alibaba", name = "arouter-compiler", version.ref = "arouter-compiler" }
    // ...

    建议是在common组件中添加依赖(common/build.gradle):

    groovy 复制代码
    dependencies {
        // ARouter依赖
        api libs.arouter.api
        // Arouter注解器依赖
        annotationProcessor libs.arouter.compiler
        // ...
    }

    最后,在其他模块依赖common模块,添加注解器参数:

    groovy 复制代码
    android {
        // ...
        defaultConfig {
            // ...
            // 配置ARouter注解器参数
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
            // ...
        }
    }
    dependencies {
        api libs.arouter.api
        annotationProcessor libs.arouter.compiler
        implementation project(':common')
        // ...
    }

    其中,配置注解器参数部分,是因为ARouter在多模块项目中工作时,需要知道每个模块的唯一名称,以便在编译期为每个模块生成独立的路由映射文件,如果不指定模块名,或者多个模块使用了相同的模块名,会导致路由注册冲突或找不到页面的问题。因此,在每个使用了ARouter注解(如@Route)的模块的build.gradle中,都需要通过AROUTER_MODULE_NAME明确指定该模块的名称。

    注:

    1. 虽然在common模块中添加了arouter和annotationProcessor注解器依赖,但为了防止编译报错,需要在每个添加了Route注解的模块下添加这两个依赖。

    2. project.getName() 可以自动获取当前模块的目录名作为模块名,避免硬编码。

    至此,业务组件中的完整build.gradle

    groovy 复制代码
    // 判断gradle.properties中是否存在isMOdule字段
    def isApplication = project.hasProperty('isModule') && project.property('isModule').toBoolean()
    
    // 根据isModule字段决定当前模块是以集成模式还是组件模式运行
    if (isApplication) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }
    
    android {
        namespace 'com.example.module_user'
        compileSdk libs.versions.compileSdk.get().toInteger()
    
    
        defaultConfig {
            // 如果是组件模式,指定Application
            if (isApplication) {
                applicationId "com.example.module_user"
            }
            minSdk libs.versions.minSdk.get().toInteger()
            targetSdk libs.versions.targetSdk.get().toInteger()
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
            consumerProguardFiles "consumer-rules.pro"
    
            // ARouter注解器配置
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
        }
    	
        // 组件模式启用manifest下完整的清单文件
        sourceSets {
            main {
                manifest.srcFile isApplication ?
                        'src/main/manifest/AndroidManifest.xml' :
                        'src/main/AndroidManifest.xml'
    			
                // 集成模式下排除debug文件夹
                if (!isApplication) {
                    java.exclude 'debug/**'
                }
            }
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_11
            targetCompatibility JavaVersion.VERSION_11
        }
    }
    
    dependencies {
        // 依赖基础common模块
        implementation project(':common')
        // ARouter和注解器依赖
        api libs.arouter.api
        annotationProcessor libs.arouter.compiler
        
        implementation libs.appcompat
        implementation libs.material
        implementation libs.activity
        implementation libs.constraintlayout
    }
  2. 初始化ARouter

    在common模块下的BaseApplication中初始化:

    java 复制代码
    package com.example.common;
    
    import android.app.Application;
    import android.content.Context;
    
    import com.alibaba.android.arouter.launcher.ARouter;
    
    public class BaseApplication extends Application {
        private static BaseApplication instance;
        @Override
        public void onCreate() {
            super.onCreate();
            instance = this;
            
            // 直接开启日志和调试模式,不依赖 BuildConfig.DEBUG(library的DEBUG恒为false)
            ARouter.openLog();
            ARouter.openDebug();
            
            // 初始化ARouter
            ARouter.init(this);
        }
    
        // 获取全局Context
        public static Context getAppContext() {
            return instance.getApplicationContext();
        }
    }
  3. 在common模块中定义路由路径(RouterPath),统一管理所有模块的ARouter路径。由于这里我只有一个模块,所有就只有一个路径:

    java 复制代码
    // common/RouterPath
    public class RouterPath {
        public static class User {
            public static final String LOGIN = "/user/login";
        }
    }

    如果模块较多,建议按业务模块分内部类,结构清晰。

  4. 目标页面添加注解:

    java 复制代码
    // 用户模块中的LoginActivity
    import com.alibaba.android.arouter.facade.annotation.Route;
    import com.example.common.RouterPath;
    
    // 添加注解
    @Route(path = RouterPath.User.LOGIN)
    public class LoginActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            EdgeToEdge.enable(this);
            setContentView(R.layout.activity_login);
        }
    }

    路径必须以 / 开头,且至少两级(如 /user/login),否则编译报错。

  5. 实现组件之间的跳转和参数传递

    在其他模块的Activity或Fragment中,如果要跳转到用户模块的LoginActivity中:

    java 复制代码
    // 跳转到登录页
    ARouter.getInstance()
        .build(RouterPath.User.LOGIN)
        .navigation();
    
    // 带参数跳转
    ARouter.getInstance()
        .build(RouterPath.User.LOGIN)
        .withString("userId", "12345")
        .withInt("age", 25)
        .navigation();
    
    // 获取返回结果(startActivityForResult)
    ARouter.getInstance()
        .build(RouterPath.User.LOGIN)
        .navigation(this, 1001); // requestCode = 1001

    如果要在目标页面拿到传递的数据:

    java 复制代码
    @Route(path = RouterPath.User.LOGIN)
    public class LoginActivity extends AppCompatActivity {
    
        @Autowired
        String userId;
        @Autowired(name = "age") // 可指定key
        int userAge;
        private static final String TAG = "ARouter";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            EdgeToEdge.enable(this);
            setContentView(R.layout.activity_login);
    
            // 自动注入参数
            ARouter.getInstance().inject(this);
            Log.d(TAG, "userId = " + userId + ", age = " + userAge);
        }
    }

    使用@Autowired标记要接收的字段,在onCreate()中调用ARouter.getInstance().inject(this);

    接收字段,字段名需与withXxx("key", value)中的key一致(或通过name指定)

    日志结果:

  6. 跨组件服务调用(IProvider)

    在非组件化项目中,如果模块A需要调用模块B的某个功能(比如获取用户信息),通常会直接引用B的类,但是这就导致编译时强依赖,无法独立编译模块、修改B模块可能影响A模块、不利于并行开发和单元测试。而跨组件服务调用就解决了这个问题,ARouter服务通过"接口+路由"解耦:A模块只依赖接口(定义在common模块),B模块提供实现,两者无直接代码依赖。

    举个例子,订单模块需要显示当前登录用户的昵称。这里我就不新建模块了,直接在app模块中显示用户昵称。大致实现思路:common模块定义IUserService接口,module_user模块实现该接口,提供getUserName();,app模块通过ARouter获取服务并调用。

    这样做的优点在于app模块无需知道用户数据如何存储(SharedPreferences / Room / 网络),只需调用接口。

    在common模块中定义服务接口:

    java 复制代码
    import com.alibaba.android.arouter.facade.template.IProvider;
    
    public interface IUserService extends IProvider {
        String getNameByID(String id);
        void login();
    }

    module_user中实现该服务:

    java 复制代码
    import android.content.Context;
    
    import com.alibaba.android.arouter.facade.annotation.Route;
    import com.example.common.IUserService;
    
    @Route(path = "/service/user")
    public class UserServiceImpl implements IUserService {
        @Override
        public String getNameByID(String id) {
            return "User_" + id;
        }
    
        @Override
        public void login() {
            // 登录逻辑
        }
    
        @Override
        public void init(Context context) {
            // ARouter会自动调用
        }
    }

    其他任何模块如果要获取用户名称,只需使用ARouter调用:

    java 复制代码
    IUserService userService = (IUserService) ARouter.getInstance()
        .build("/service/user")
        .navigation();
    
    if (userService != null) {
        String name = userService.getNameByID("12345");
        Log.d(TAG, "name = " + name);
    }

    或者使用更简洁的方式:

    java 复制代码
    IUserService userService = ARouter.getInstance().navigation(IUserService.class);

    查看日志,成功获取到用户名称:

3.6 组件之间资源名冲突

因为拆分出了很多业务组件和功能组件,在把这些组件合并到app壳工程时就有可能出现资源名冲突问题。比如A组件和B组件都有一张叫做"ic_back"的图标,这时候在集成模式下打包app就会编译出错,解决这个问题最简单的做法就是在项目中约定资源文件命名规则,比如强制使每个资源文件的名称以组件名开始,这个可以根据实际情况和开发人员制定规则。当然Gradle也提供了解决办法,通过在组件的build.gradle中添加如下的代码:

groovy 复制代码
android {
	resourcePrefix "user_" // 注意:必须以小写字母开头,结尾加下划线
}

规则:

  • 必须是小写字母开头。
  • 通常以模块名 + _结尾(如user_order_
  • 所有res/下的资源(layout,drawable,string)等必须以此开头,否则编译报错。

4. 总结

  1. 架构分层:自顶向下为App壳工程、业务组件、功能组件、Common基础库,上层依赖下层,实现解耦。
  2. 模式切换 :通过gradle.properties中的isModule开关,控制业务组件在可独立运行的组件模式 与作为依赖库的集成模式间切换。
  3. 统一配置:推荐使用Gradle Version Catalog (TOML) 统一管理所有依赖版本,避免冲突。
  4. 清单与全局Context :采用双套AndroidManifest.xml解决清单合并冲突;通过Common模块的BaseApplication提供统一的全局Context。
  5. 通信方案 :引入ARouter路由框架 ,通过注解和路径实现跨组件页面跳转与参数传递;通过IProvider接口实现跨组件服务调用,彻底解耦。
  6. 工程细节 :通过resourcePrefix或命名规约规避资源冲突,并配置ARouter的模块名以保证路由表正确生成。
相关推荐
心有—林夕2 小时前
MySQL 误操作恢复完全指南
android·数据库·mysql
忙什么果3 小时前
Mamba学习笔记2:Mamba模型
android·笔记·学习
Wyawsl3 小时前
MySQL故障排查与优化
android·adb
私人珍藏库5 小时前
[Android] 后台视频录制 FadCam v3.0.1
android·app·工具·软件·多功能
Z_Wonderful5 小时前
在 **Next.js** 中使用 `mysql2` 连接 MySQL 数据库并查询 `xxx` 表的数据
android·数据库
FirstFrost --sy5 小时前
MySql 内外连接
android·数据库·mysql
激昂网络5 小时前
在Ubuntu 24.04上编译T527 Android系统:遇到的几个问题及解决方法
android·linux·ubuntu
李艺为6 小时前
android客制开发之DevCheck检测CPU核心作假
android
hnlgzb6 小时前
LiveData和MutableLiveData都是什么?有什么区别?都是在什么情况下用?
android