【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的模块名以保证路由表正确生成。
相关推荐
AFinalStone2 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
宝杰X73 小时前
Android Room3 多平台数据库
android·数据库
huibin1478523696 小时前
热缓解学习记录
android·学习
小田的博客9 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆10 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
只会cv的小前端12 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
12 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白12 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
hunterandroid14 小时前
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
android
我命由我1234517 小时前
Jetpack Compose - Material Design 断点范围、WindowSizeClass、针对不同屏幕尺寸创建预览、四类导航栏
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime