Android 模块化与组件化工程实战:从子模块库化、Gradle 配置统一到 ARouter 解耦跨模块页面通信与 Fragment 解耦集成

Android 模块化与组件化工程实战:从子模块库化、Gradle 配置统一到 ARouter 跨模块页面通信与 Fragment 解耦集成


前言

本文围绕一个典型的 Android 多模块工程展开,先把多个可独立运行的功能模块收拢到主应用中,再补齐公共基础库、统一 Gradle 配置,最后用 ARouter 完成跨模块页面跳转、参数传递以及跨模块 Fragment 装配。整篇内容按工程改造的实际推进顺序展开,重点不是只看结论,而是把每一步为什么做、改了哪里、模块之间如何协同交代清楚。

模块化的核心目标是按功能边界拆分工程,让功能模块可以独立开发、测试与维护;组件化更强调公共能力的封装与复用。放到这套工程里,feature_userfeature_findfeature_homefeature_plaza 负责业务能力,library_base 负责沉淀公共依赖与基础能力,这样主模块只需要负责组装,而不必承担全部实现细节。

目录

  • [Android 模块化落地与 ARouter 解耦实战:从子模块改造到跨模块页面与 Fragment 跳转](#Android 模块化落地与 ARouter 解耦实战:从子模块改造到跨模块页面与 Fragment 跳转)
  • [1. 项目模块化](#1. 项目模块化)
  • [2. 项目模块化时遇到的问题](#2. 项目模块化时遇到的问题)
  • [3. 项目模块化中的基础核心模块](#3. 项目模块化中的基础核心模块)
  • [4. 统一管理gradle的配置](#4. 统一管理gradle的配置)
  • [5. 使用ARouter进行module间的通信](#5. 使用ARouter进行module间的通信)
  • [6. 使用ARouter进行页面跳转](#6. 使用ARouter进行页面跳转)
  • [7. 为什么要在模块化设计中用ARouter](#7. 为什么要在模块化设计中用ARouter)
  • [8. ARouter的其他用法](#8. ARouter的其他用法)
    • [8.1 跳转](#8.1 跳转)
    • [8.2 不同模块直接解耦前提下引入跨模块 Fragment](#8.2 不同模块直接解耦前提下引入跨模块 Fragment)
  • [9. 相关代码附录](#9. 相关代码附录)
    • [9.1 app 与根工程配置](#9.1 app 与根工程配置)
    • [9.2 功能模块与基础库配置](#9.2 功能模块与基础库配置)
    • [9.3 ARouter 初始化与页面跳转](#9.3 ARouter 初始化与页面跳转)
    • [9.4 跨模块 Fragment 装配](#9.4 跨模块 Fragment 装配)

1. 项目模块化

模块化改造的第一步,是让主模块显式依赖各个功能模块。这样 app 在打包时才能把业务能力统一组装起来,同时各个模块仍然保留清晰的功能边界。

app/build.gradle 中,先把四个功能模块加入 dependencies

groovy 复制代码
dependencies {

    implementation libs.appcompat
    implementation libs.material
    implementation libs.activity
    implementation libs.constraintlayout
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core

    implementation project(":feature_user")
    implementation project(":feature_find")
    implementation project(":feature_home")
    implementation project(":feature_plaza")
}

这一步的作用很直接:app 从"只包含自身代码"的单模块工程,变成"统一引用多个业务模块"的聚合入口。implementation project(":feature_user") 这一类声明表示把对应模块作为编译期和运行期依赖引入主工程,后续页面、资源和清单合并也都会围绕这个依赖关系展开。

不过,模块之间存在依赖关系,不等于它们已经具备"可被主模块正常集成"的状态。当前四个 feature 模块最初都是能单独运行的应用模块,它们自身带有 applicationId、应用插件和独立入口,这些特征在独立调试时有价值,但在作为依赖库合并进 app 时会立刻引出新的冲突。

2. 项目模块化时遇到的问题

当多个功能模块原本都是独立 app 时,主模块虽然可以通过依赖访问它们的类,但不能把多个应用模块直接打包成一个应用。因为 com.android.application 语义上表示"这是最终 APK 的入口模块",一个最终应用只能有一个真正的应用模块,其余被集成的部分必须转换成库模块。

因此,接下来的核心任务不是继续增加依赖,而是把这些功能模块从"独立运行的应用"切换为"由主模块统一集成的依赖库"。

  • 来到子模块的 build.gradle,点击图中的代码跳转。
  • 跳转到工程目录的 gradle 目录的版本配置文件中。
  • 如果要把子模块从可独立运行的应用模块切换为可被主模块集成的依赖库,首先要有对应的库插件别名。

ComponentByJavaProject/gradle/libs.versions.toml 中,这两个插件别名分别对应:

toml 复制代码
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }

androidApplication 表示模块具备独立安装、独立启动和独立 applicationId 的能力;androidLibrary 则表示模块只提供代码、资源和清单片段,由上层应用统一打包。

  • 修改好版本配置后,再回到子模块声明插件的位置,把模块插件切换成可按条件选择的形式。

四个功能模块统一采用下面的写法:

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

这里的关键不只是"从应用插件改成库插件",而是通过 rootProject.ext.isModule 保留双态切换能力。true 时模块可以单独运行,便于局部调试;false 时模块作为依赖库参与主工程打包,适合联调和最终集成。

  • 同时,在项目根目录的 build.gradle 中,需要新增统一配置项。

根工程对应的配置如下:

groovy 复制代码
ext {

    isModule = false

    compileSdkVersion = 34
    minSdkVersion = 26
    targetSdkVersion = 34
    versionCode = 1
    versionName = "1.0"

    javaCompileOptions = [
            sourceCompatibility: JavaVersion.VERSION_1_8,
            targetCompatibility: JavaVersion.VERSION_1_8
    ]
}

这段配置承担了两层职责。第一层是用 isModule 统一控制模块运行形态;第二层是把 compileSdkVersionminSdkVersiontargetSdkVersionJava 编译版本上提到根工程,避免每个模块各写一套,后期出现版本漂移。

  • 完成这些调整后,子模块会从独立应用的形态切换为库模块形态。

这个变化不是界面层面的细节,而是工程角色发生了切换。切换成功后,模块不再试图以单独应用身份安装运行,而是等待被 app 统一合并。

  • 其他功能模块的 build.gradle 也要同步改成同样的配置。

如果只有一个模块完成切换,而其他同级模块仍保留应用插件,主工程在打包阶段依然会面对多个应用模块冲突,所以这一步必须横向同步到 feature_findfeature_homefeature_plazafeature_user

  • 从独立应用切换为依赖库后,需要注释掉或移除 applicationId

功能模块里的 defaultConfig 统一写成下面这样:

groovy 复制代码
defaultConfig {
    if (rootProject.ext.isModule) {
        applicationId "com.ls.fuser"
    }
    minSdk rootProject.ext.minSdkVersion
    targetSdk rootProject.ext.targetSdkVersion
    versionCode rootProject.ext.versionCode
    versionName rootProject.versionName
}

原因在于 applicationId 只属于最终可安装应用。库模块没有独立包名入口,只保留 namespace 用于资源和 R 类生成。如果在库模块模式下仍强行声明 applicationIdGradle 会直接报错。

  • 同时,还要把子模块的 minSdk 与主模块保持一致,避免依赖冲突。

minSdktargetSdkcompileSdk 一类参数一旦在不同模块中不一致,最常见的问题就是清单合并失败、依赖能力不兼容,或者编译通过但运行行为不稳定。把这些值集中由根工程统一管理,本质上是在为整个多模块工程建立一套稳定的构建基线。

3. 项目模块化中的基础核心模块

当主模块和业务模块的关系理顺之后,工程里还需要一层公共基础库来承接通用能力。否则,网络请求、路由依赖、工具类或基础封装会被重复散落在多个业务模块里,模块边界虽然存在,复用却仍然混乱。

这类基础库可以被多个功能模块共同依赖,再由 app 统一组装,因此适合作为模块化和组件化之间的连接层。

公共库的创建方式与功能模块不同,它直接以 Android Library 的形式创建:

创建完成后,library_base 会天然使用 androidLibrary,也不会要求单独配置 applicationId。这说明它从一开始就是"被依赖的公共能力模块",而不是"可独立安装的业务入口"。

如果工程最初只有主模块,后来新增 Android Library,构建系统还会自动把对应的库插件别名补到版本配置里:

随后,主模块和功能模块都可以通过依赖公共库来共享能力:

ComponentByJavaProject/library_base/build.gradle 中,基础库除了标准 Android 依赖外,还承担了公共路由依赖的出口职责:

groovy 复制代码
dependencies {

    implementation libs.appcompat
    implementation libs.material
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core

    api 'com.alibaba:arouter-api:1.5.2'
    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}

这里使用 api 而不是 implementation 有明确目的。api 暴露给下游模块后,像 feature_userfeature_find 这样的业务模块在依赖 library_base 时,就能直接访问 ARouter 的运行时能力;如果这里只写 implementation,下游模块无法透传获得这些类,仍然需要自己重复声明依赖。

把公共能力放进基础库,本质上是在做一层组件化沉淀。模块化解决的是工程按功能拆分,组件化解决的是公共能力如何稳定复用,这也是为什么基础库通常会成为整个工程里最值得长期维护的一层。

将通用能力汇总到公共依赖库后,不同模块只需要按需引用,不必重复维护同一套实现:

4. 统一管理 gradle 的配置

模块数量一多,最容易失控的就是 Gradle 配置重复。相同的 SDK 版本、编译选项、版本号、注解处理参数如果散落在每个模块中,后续任何一次升级都要逐个修改,既费时也容易漏改。

所以接下来要做的不是继续增加局部配置,而是把公共构建参数统一抽取到根工程。

先在根工程的 build.gradle 中定义公共配置:

groovy 复制代码
ext {

    minSdkVersion = 26
}

然后在子模块里通过 rootProject.ext 读取:

groovy 复制代码
android {
    namespace 'com.ls.fuser'
    compileSdk rootProject.ext.compileSdkVersion
}

进一步完善后,可以把整套公共构建参数全部上提:

groovy 复制代码
ext {

    compileSdkVersion = 34
    minSdkVersion = 26
    targetSdkVersion = 34
    versionCode = 1
    versionName = "1.0"

    javaCompileOptions = [
            sourceCompatibility: JavaVersion.VERSION_1_8,
            targetCompatibility: JavaVersion.VERSION_1_8
    ]
}

子模块的完整读取方式如下:

groovy 复制代码
android {
    namespace 'com.ls.fuser'
    compileSdk rootProject.ext.compileSdkVersion

    defaultConfig {

        minSdk rootProject.ext.minSdkVersion
        targetSdk rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility rootProject.ext.javaCompileOptions.sourceCompatibility
        targetCompatibility rootProject.ext.javaCompileOptions.targetCompatibility
    }
}

这一段最值得关注的是三类信息的统一来源:

  • 第一类是平台参数,像 compileSdkminSdktargetSdk 决定整个工程基于哪套 Android 平台能力编译和运行。
  • 第二类是版本参数,像 versionCodeversionName 影响安装包版本管理。
  • 第三类是编译参数,像 sourceCompatibilitytargetCompatibilityannotationProcessorOptions 会直接影响注解处理和 Java 编译行为。

如果这些参数由模块各自维护,一旦某个模块少配了 annotationProcessorOptionsARouter 就可能无法正确生成该模块的路由表;如果某个模块的 SDK 版本偏低,又会造成整包集成冲突。

除了集中管理公共配置,还可以把"模块是否独立运行"的切换逻辑也抽到根工程:

groovy 复制代码
ext {

    isModule = false
}

然后在子模块里用它统一决定插件和 applicationId

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

android {

    defaultConfig {
        if (rootProject.ext.isModule) {
            applicationId "com.ls.fuser"
        }
    }
}

这样一来,日常开发想单独调试某个功能模块时,只需要把 isModule 改成 true;需要合并回主工程打包联调时,再切回 false。它不是简单的开关,而是整个多模块工程构建模式的统一入口。

5. 使用 ARouter 进行 module 间的通信

模块拆开以后,新的问题会立刻出现:页面之间如何通信?如果跨模块跳转仍然沿用直接 import 目标页面类的方式,模块边界虽然还在,耦合关系却已经重新长回来。

这时需要引入路由层,让调用方只依赖一条字符串路径,而不是依赖目标类本身。ARouter 的价值就在这里,它把"跳转目标是谁"从直接类引用,改成了运行时通过路由表查找。

先在 library_base 中加入 ARouter 依赖:

groovy 复制代码
dependencies {

    implementation libs.appcompat
    implementation libs.material
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core

    api 'com.alibaba:arouter-api:1.5.2'
    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}

api 'com.alibaba:arouter-api:1.5.2' 负责提供运行时跳转能力,annotationProcessor 'com.alibaba:arouter-compiler:1.5.2' 负责在编译期扫描 @Route@Autowired 等注解并生成路由映射代码。两者缺一不可,前者没有时运行期找不到 ARouter 类,后者没有时虽然代码能写,但路由表不会被正确生成。

接着,在每个需要参与路由表生成的模块里补上注解处理参数:

groovy 复制代码
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [AROUTER_MODULE_NAME: project.getName()]
    }
}

这里的 AROUTER_MODULE_NAME 会把当前模块名传给注解处理器,用来生成该模块自己的路由表文件。因为每个 feature 模块都可能暴露页面或 Fragment,所以这段配置不能只出现在主模块,所有涉及路由声明的模块都要具备。

如果接入 ARouter 后出现 android.supportAndroidX 冲突,还需要在 gradle.properties 中开启 Jetifier

properties 复制代码
android.enableJetifier = true

这行配置的作用是把三方库里旧的支持库引用自动迁移成对应的 AndroidX 实现,避免构建阶段出现重复类冲突。由于 ARouter 老版本依赖链中可能仍包含旧包名,这一步在旧工程接入里尤其重要。

同时,compileSdkVersion 也要使用较新的版本:

groovy 复制代码
ext {

    isModule = false

    compileSdkVersion = 34
}

编译平台过低时,一方面会影响新版本依赖解析,另一方面也容易让旧构建脚本与新路由依赖组合出兼容性问题。因此把 compileSdkVersion 一并提升到统一配置里,是这一步的必要配套。

最后,在主应用的 Application 中完成 ARouter 初始化:

java 复制代码
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            ARouter.openLog();
            ARouter.openDebug();
        }

        ARouter.init(this);
    }
}

这里的调用顺序不能乱。ARouter.openLog()ARouter.openDebug() 必须在 ARouter.init(this) 之前执行,因为它们是在初始化前设置调试开关;而 ARouter.init(this) 则会在应用启动阶段扫描并注册所有带有 @Route 的类,把"路径"和"目标类"对应起来。

要让这个 Application 真正生效,还必须在 app 的清单中指定:

xml 复制代码
<application
    android:name=".MyApplication"

只有这样,系统启动应用时才会先执行 MyApplication.onCreate(),路由框架也才能在第一时间完成全局初始化。如果这里漏掉 android:name,后续 navigation() 时常见的现象就是路由未注册或注入失败。

如果上面这些配置都已经完成,但运行时仍然报错,最直接的排查方式就是卸载重装应用。因为路由表、清单合并结果和安装缓存都可能残留旧状态,重装往往能清掉历史缓存带来的干扰。

6. 使用 ARouter 进行页面跳转

完成基础接入后,就可以把跨模块页面跳转从直接类引用改成路由方式。这样调用方不再需要 import 目标页面类,模块之间的依赖关系就会从"编译期直接依赖页面实现"变成"运行时依赖路由协议"。

先在入口页面里通过 ARouter 发起跳转,并携带参数:

java 复制代码
findViewById(R.id.tv_label).setOnClickListener(veiw -> {

    ARouter.getInstance()
            .build("/feature_user/UserActivity")
            .withString("key", "你好 我是MainActivity!")
            .navigation();
});

这段代码里有三个关键信息:

  • build("/feature_user/UserActivity") 用来指定目标路由路径,推荐保持 "/模块名/页面名" 的格式,这样路径天然具备业务归属。
  • withString("key", "你好 我是MainActivity!") 负责往路由对象里附加参数,内部最终会转成目标页面可读取的额外数据。
  • navigation() 则是真正执行跳转的动作,前面构建的路由信息到这里才会被提交给框架处理。

目标页面需要用 @Route 显式声明自己的路由身份:

java 复制代码
@Route(path = "/feature_user/UserActivity")
public class UserActivity extends AppCompatActivity {

这条路径必须全局唯一,因为 ARouter 在初始化时会把路径和目标类注册成一一映射关系。如果两个页面声明同一路径,运行期就无法唯一定位跳转目标。

跳转参数可以先用最直接的方式获取:

java 复制代码
String mString = getIntent().getStringExtra("key");

Log.i(TAG, "onCreate: mString = " + mString);

if (mString != null && !mString.isEmpty()) {
    tvLabel.setText(mString);
}

这段读取逻辑说明了参数的数据流转链路:

  • 源页面在 withString("key", ...) 中写入,ARouter 在跳转时把它附着到 Intent 上,目标页面再通过 getIntent().getStringExtra("key") 取出。
  • 读取成功后,tvLabel 的内容会从默认文本改成跳转时传入的字符串。

参考工程中的 MainActivity 还保留了一句直接跳转:

java 复制代码
startActivity(new Intent(this, HomeActivity.class));

这句代码正好可以作为对比。它依赖了 com.ls.fhome.HomeActivity 的直接导入,意味着入口模块必须知道目标模块的具体类名,模块间会形成硬编码引用;而 ARouter 的路由跳转只依赖路径字符串,更符合解耦目标。

如果页面不能正常跳转,依旧优先考虑缓存问题,卸载重装后再验证结果。

7. 为什么要在模块化设计中用ARouter

在模块化工程里引入 ARouter,本质上不是为了"换一种跳转 API",而是为了把跨模块调用从类依赖改成协议依赖。

应用启动时,Application 会先执行,这也是 ARouter 初始化最合适的时机:

java 复制代码
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            ARouter.openLog();
            ARouter.openDebug();
        }

        ARouter.init(this);
    }
}
  • ARouter.init(this) 执行时,框架会把工程中所有被 @Route 标记的类扫描并注册起来。
  • 后续调用 build("/feature_user/UserActivity").navigation() 时,框架实际做的是"根据路径查询目标类,再完成跳转",而不是"依赖某个模块的具体页面类直接启动"。

这会带来两个直接收益:

  • 第一,调用方不需要导入目标页面类,跨模块跳转不再产生直接编译依赖。
  • 第二,模块边界更加稳定。只要路由路径不变,目标页面内部如何重构、包名如何调整、是否替换实现,调用方都不必同步修改。

这也是为什么在多业务模块并行开发时,路由层经常被视为模块通信的基础设施。它把模块之间最容易扩散的"页面级硬依赖"收束成了统一的调用入口。

8. ARouter的其他用法

完成基础页面跳转后,ARouter 还可以继续承担参数自动注入和跨模块 Fragment 获取的工作。它们的共同价值在于继续减少模块之间对具体实现类的直接依赖。

8.1 跳转

前面参数读取使用的是 Intent 方式:

java 复制代码
String string = getIntent().getStringExtra("key");

如果希望让参数接收逻辑更集中,可以在目标页面的 onCreate() 中先执行自动注入:

java 复制代码
ARouter.getInstance().inject(this);

然后声明成员变量,并用 @Autowired 绑定参数名:

java 复制代码
@Autowired(name = "key")
public String mString;

这套写法的执行链路是这样的:

  • 跳转方通过 withString("key", ...) 传入参数;
  • 页面创建后调用 ARouter.getInstance().inject(this),框架再把名为 key 的值注入到 mString 字段中;

这样参数读取从"手动逐个 getExtra"变成了"按字段声明自动注入",更适合参数较多的页面。

需要注意两点:

  • 第一,inject(this) 必须在读取字段之前执行,否则 mString 还没有被赋值。
  • 第二,被 @Autowired 标记的成员变量不能声明为 private,否则框架无法完成注入。

参考工程中的 UserActivity 就是按这个方式处理参数的:

java 复制代码
@Autowired(name = "key")
public String mString;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_user);

    ARouter.getInstance().inject(this);

    TextView tvLabel = findViewById(R.id.tv_label);
    Log.i(TAG, "onCreate: mString = " + mString);

    if (mString != null && !mString.isEmpty()) {
        tvLabel.setText(mString);
    }
}

这里 tvLabel 的显示内容,既是参数注入是否成功的界面反馈,也是跳转链路是否打通的最直接验证结果。

8.2 不同模块直接解耦前提下引入跨模块 Fragment

除了页面跳转,ARouter 还可以把某个模块中的 Fragment 暴露给其他模块使用。这样,调用方只知道"我要拿一个符合某条路由路径的 Fragment",而不需要依赖目标模块里具体的 Fragment 类。

先在功能模块中定义 Fragment 并声明路由:

java 复制代码
@Route(path = "/feature_find/FindFragment")
public class FindFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_find, container, false);
    }
}

FindFragment 的职责很清晰,它只负责提供自己的视图实现,而对外暴露的识别方式不再是类名,而是 "/feature_find/FindFragment" 这条路径。

跨模块宿主页面要接入它,先在布局中提供一个容器:

xml 复制代码
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fcv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

这个 FragmentContainerView 的作用,是给后续动态添加的 Fragment 提供真实挂载位置。没有它,即使路由能拿到 Fragment 实例,也没有地方展示。

然后通过 ARouter 获取 Fragment 实例,并与容器关联:

java 复制代码
Fragment findFragment = (Fragment) ARouter.getInstance()
        .build("/feature_find/FindFragment")
        .navigation();

if (findFragment != null) {
    getSupportFragmentManager().beginTransaction()
            .add(R.id.fcv, findFragment)
            .commit();
}

这里有一个非常关键的细节:接收类型写成 Fragment,而不是 FindFragment。原因并不是写法随意,而是为了继续保持解耦。如果把变量声明为 FindFragment,宿主模块就必须导入目标模块中的具体类,跨模块依赖会立刻重新建立;而使用 AndroidX 的通用 Fragment 类型,只保留路由路径依赖,模块边界才能真正稳定下来。

参考工程中的 activity_user.xmlUserActivity 正好形成完整闭环。布局里声明了 FragmentContainerView

xml 复制代码
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fcv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

页面中再通过路由把 FindFragment 动态挂进来:

java 复制代码
Fragment findFragment = (Fragment) ARouter.getInstance()
        .build("/feature_find/FindFragment")
        .navigation();

if (findFragment != null) {
    getSupportFragmentManager().beginTransaction()
            .add(R.id.fcv, findFragment)
            .commit();
}

执行成功后,UserActivity 一方面会通过 tv_label 显示上一个页面传来的字符串,另一方面还会在 fcv 容器里展示 FindFragment 的内容。参数注入与跨模块 Fragment 装配同时生效,正好验证了路由层不仅能做页面跳转,也能承担更细粒度的模块间协作。

如果这里无法正常展示,排查顺序通常是三步:先检查 @Route(path = "/feature_find/FindFragment") 是否与调用路径完全一致,再检查模块里是否声明了注解处理参数,最后卸载重装应用排除缓存干扰。

9. 相关代码附录

下面把正文里涉及到的关键代码文件按职责汇总,便于结合工程结构快速定位。

9.1 app 与根工程配置

文件 ComponentByJavaProject/build.gradle

groovy 复制代码
ext {

    isModule = false

    compileSdkVersion = 34
    minSdkVersion = 26
    targetSdkVersion = 34
    versionCode = 1
    versionName = "1.0"

    javaCompileOptions = [
            sourceCompatibility: JavaVersion.VERSION_1_8,
            targetCompatibility: JavaVersion.VERSION_1_8
    ]
}

文件 ComponentByJavaProject/app/build.gradle

groovy 复制代码
plugins {
    alias(libs.plugins.androidApplication)
}

android {
    namespace 'com.ls.componentbyjavaproject'
    compileSdk rootProject.ext.compileSdkVersion

    defaultConfig {
        applicationId "com.ls.componentbyjavaproject"
        minSdk rootProject.ext.minSdkVersion
        targetSdk rootProject.ext.targetSdkVersion
        versionCode  rootProject.ext.versionCode
        versionName rootProject.versionName

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    implementation project(":feature_user")
    implementation project(":feature_find")
    implementation project(":feature_home")
    implementation project(":feature_plaza")
    implementation project(":library_base")
    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}

文件 ComponentByJavaProject/gradle.properties

properties 复制代码
android.useAndroidX=true
android.nonTransitiveRClass=true
android.enableJetifier = true

9.2 功能模块与基础库配置

文件 ComponentByJavaProject/gradle/libs.versions.toml

toml 复制代码
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }

文件 ComponentByJavaProject/library_base/build.gradle

groovy 复制代码
plugins {
    alias(libs.plugins.androidLibrary)
}

dependencies {
    api 'com.alibaba:arouter-api:1.5.2'
    annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}

文件 ComponentByJavaProject/feature_user/build.gradle,其余 feature 模块结构一致:

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

android {
    namespace 'com.ls.fuser'
    compileSdk rootProject.ext.compileSdkVersion

    defaultConfig {
        if (rootProject.ext.isModule) {
            applicationId "com.ls.fuser"
        }
        minSdk rootProject.ext.minSdkVersion
        targetSdk rootProject.ext.targetSdkVersion

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

9.3 ARouter 初始化与页面跳转

文件 ComponentByJavaProject/app/src/main/java/com/ls/componentbyjavaproject/MyApplication.java

java 复制代码
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        if (BuildConfig.DEBUG) {
            ARouter.openLog();
            ARouter.openDebug();
        }

        ARouter.init(this);
    }
}

文件 ComponentByJavaProject/app/src/main/AndroidManifest.xml

xml 复制代码
<application
    android:name=".MyApplication"

文件 ComponentByJavaProject/app/src/main/java/com/ls/componentbyjavaproject/MainActivity.java

java 复制代码
findViewById(R.id.tv_label).setOnClickListener(veiw -> {

    ARouter.getInstance()
            .build("/feature_user/UserActivity")
            .withString("key", "你好 我是MainActivity!")
            .navigation();

    startActivity(new Intent(this, HomeActivity.class));
});

文件 ComponentByJavaProject/feature_user/src/main/java/com/ls/fuser/UserActivity.java

java 复制代码
@Route(path = "/feature_user/UserActivity")
public class UserActivity extends AppCompatActivity {

    @Autowired(name = "key")
    public String mString;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        ARouter.getInstance().inject(this);

        TextView tvLabel = findViewById(R.id.tv_label);

        Log.i(TAG, "onCreate: mString = " + mString);

        if (mString != null && !mString.isEmpty()) {
            tvLabel.setText(mString);
        }
    }
}

9.4 跨模块 Fragment 装配

文件 ComponentByJavaProject/feature_find/src/main/java/com/ls/ffind/FindFragment.java

java 复制代码
@Route(path = "/feature_find/FindFragment")
public class FindFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_find, container, false);
    }
}

文件 ComponentByJavaProject/feature_user/src/main/res/layout/activity_user.xml

xml 复制代码
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fcv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

文件 ComponentByJavaProject/feature_user/src/main/java/com/ls/fuser/UserActivity.java

java 复制代码
Fragment findFragment = (Fragment) ARouter.getInstance()
        .build("/feature_find/FindFragment")
        .navigation();

if (findFragment != null) {
    getSupportFragmentManager().beginTransaction()
            .add(R.id.fcv, findFragment)
            .commit();
}
相关推荐
JMchen1232 小时前
高级渲染技术:OpenGL ES在自定义View中的应用
android·性能优化·3d渲染·opengl es·自定义view·glsurfaceview·shader编程
鹧鸪晏2 小时前
搞懂 kotlin 泛型 out 和 in 关键字
android·kotlin
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Android的“旧时光”书店App为例,包含答辩的问题和答案
android
hashiqimiya2 小时前
androidstudio历史版本
android
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于Android的出租车运行监测系统设计与实现为例,包含答辩的问题和答案
android
fatiaozhang95272 小时前
晶晨S905W2芯片_sbx_x98_plus_broagcon_atv_安卓11_线刷包固件包
android·电视盒子·刷机固件·机顶盒刷机·机顶盒刷机固件大全·晶晨s905w2芯片
匆忙拥挤repeat2 小时前
Android Compose 依赖配置解读
android
没有了遇见3 小时前
Android 关于注入Js处理Android和H5 Js 交互问题
android
阿拉斯攀登3 小时前
第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例
android·驱动开发·rk3568·瑞芯微·嵌入式驱动·安卓驱动·spi 设备驱动