Android 组件化开发基础实践

安卓开发中,在项目做大之后,一个 超级app 模块的痛点就会越来越明显,编译时间长,各个业务模块高度耦合,项目复用度低(想把一个业务挪到另一个 App 很麻烦)等弊端。于是就有了 组件化开发,把一个"大工程"拆成多个相对独立的业务模块(组件),通过统一的路由通信、公共基础库把它们拼起来。本篇文章会从 0 开始介绍组件化,尽量一篇文章让大家都能写出符合组件化规范的代码。


一、什么是组件化?

组件化就是把一个大 App 拆成多个独立的业务模块(组件),每个组件尽量能"自己跑起来",再通过壳工程把它们组合成一个完整的 App。

常见的结构大概是这样(以一个电商软件为例):

text 复制代码
app/                 // 壳工程、最终安装的 APK
common/              // 公共基础库:utils、网络、日志、通用 UI 等
module_home/         // 首页模块
module_user/         // 用户模块(登录注册、个人中心)
module_mall/         // 商城模块
module_xxx/          // 其它业务模块

组件化的核心在于以下三点:

  • 业务边界清晰:每个组件都有比较完整的一条业务逻辑
  • 可以独立调试 :比如只启动 module_user 来调登录功能,不用整个 App 都跑起来
  • 组件之间通过路由 / 接口通信,尽量减少直接依赖其他模块实现类

二、项目如何拆模块?一个基础的组件化结构

下面给一个示例 Gradle 结构,用 单工程多 Module 的方式实现组件化。

2.1 顶层 settings.gradle 配置

groovy 复制代码
include ':app'
include ':common'
include ':module_home'
include ':module_user'
include ':module_mall'

2.2 各模块职责简要说明

  • app是真正打包的入口,这实际上就是常说的组件化的壳工程 ,负责1. 组装"所有业务组件 ,依赖各个业务模块:module_homemodule_usermodule_mall,但尽量在该 模块中少写业务代码,只负责入口和整体导航。2. 做全局初始化,比如:ARouter 初始化、网络库初始化、日志、Crash、埋点等以及全局 Theme、字体、语言的处理。

    维护"真正的" Application 和启动入口

  • common一般作为基础工具模块,包含

    • 基础工具类:网络封装、日志封装、图片加载封装等
    • 一些全局常量、基础 UI 组件(BaseActivityBaseFragment
    • 路由常量(例如 ARouter 的 path 集中管理)
  • module_home / module_user / module_mall 这些属于业务模块,每个模块只负责自己那条业务线的界面和逻辑。对外只暴露"入口"和"接口",不暴露内部具体实现


三、ARouter 简介与基础配置

在组件化里,一个最核心的问题就是,A 模块跳转 B 模块的页面该怎么做,直接写 startActivity(Intent(this, BActivity::class.java))无法在两个模块间跳转,如果导入依赖又产生了明显的编译时依赖 ,模块之间紧紧绑死。

而路由框架 ARouter 提供的是一种 "字符串路径跳转 + 自动注入" 的方式。

3.1 引入 ARouter 依赖

以 Kotlin + Gradle(Android Gradle Plugin 8.x 左右)为例,在顶层 build.gradle 里加上 ARouter 的 classpath(新版一般不用,直接在模块中引依赖即可,这里以常用写法为例),**在各个需要使用路由的模块(如 appmodule_homemodule_user)**的 build.gradle 中添加:

groovy 复制代码
dependencies {
    implementation "com.alibaba:arouter-api:1.5.2"
    kapt "com.alibaba:arouter-compiler:1.5.2"
}

并开启注解处理:

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

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

Java 项目用 annotationProcessor;Kotlin 用 kapt

3.2 在 Application 中初始化 ARouter

通常我们在 app 模块的 Application 中初始化 ARouter

kotlin 复制代码
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) {
            ARouter.openLog()     // 打印日志
            ARouter.openDebug()   // 开启调试模式(InstantRun 下必须开启)
        }

        ARouter.init(this)
    }
}

并在 AndroidManifest.xml 中指定:

xml 复制代码
<application
    android:name=".MyApp"
    ... >
    ...
</application>

到这里,ARouter 的基础环境就 OK 了。


四、组件间页面跳转:用 ARouter 路由通信

下面我们以一个简单场景为例:

  • module_home 中有一个 HomeActivity
  • module_user 中有一个 UserCenterActivity
  • 点击首页按钮跳转到用户中心,并把用户 ID 传过去

4.1 在 User 模块中定义路由页面

module_user 里,创建一个 UserCenterActivity

kotlin 复制代码
@Route(path = "/user/center")
class UserCenterActivity : AppCompatActivity() {

    @Autowired
    @JvmField // 可选,为 Java 兼容
    var userId: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user_center)

        // 注入 @Autowired 的字段
        ARouter.getInstance().inject(this)

        // 这里你就可以使用 userId 做初始化,比如请求用户信息
        findViewById<TextView>(R.id.tvUserId).text = "当前用户ID:$userId"
    }
}

关键点:

  • @Route(path = "/user/center"):定义路由路径,全局唯一
  • @Autowired var userId: String?:标记这个字段从路由参数中自动注入
  • ARouter.getInstance().inject(this):在 onCreate 里注入参数

4.2 在 Home 模块中发起跳转

module_home 中的某个页面(如 HomeActivity),点击按钮跳转:

kotlin 复制代码
class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        findViewById<Button>(R.id.btnGoUserCenter).setOnClickListener {
            ARouter.getInstance()
                .build("/user/center")
                .withString("userId", "123456")
                .navigation()
        }
    }
}

说明:

  • build("/user/center"):构建目标路由,路径对应 UserCenterActivity 上的 @Route 标注
  • .withString("userId", "123456"):携带参数,key 要和 @Autowired 字段名一致
  • .navigation():真正执行跳转

这样,模块之间只通过 字符串路径 来通信,不用直接依赖类名,模块间耦合度明显降低。


五、组件间"服务"通信:用 ARouter 暴露接口服务

除了页面跳转,组件化里还有一个很常见的需求:A 模块需要调用 B 模块的一些能力,但并不是简单"打开一个页面"。比如

  • User 模块暴露"获取当前登录用户信息"的能力
  • Mall 模块暴露"下单、查询订单"的能力

如果直接依赖 B 模块里的 UserManager 实现类,又会形成强耦合。
ARouter 也提供了"服务路由"的方式来做解耦。

5.1 定义服务接口(可放在 common)

common 模块里定义一个接口:

kotlin 复制代码
interface IUserService : IProvider {

    fun isLogin(): Boolean

    fun getUserId(): String?
}

注意:继承 IProvider,这是 ARouter 服务类的基础接口。

5.2 在 User 模块中实现服务

module_user 中实现这个接口,并用 @Route 标注:

kotlin 复制代码
@Route(path = "/service/user")
class UserServiceImpl : IUserService {

    private var login = false
    private var userId: String? = null

    override fun init(context: Context?) {
        // 这里做一些初始化操作,比如加载本地用户信息
        // 这个方法会在 ARouter 初始化后自动调用
    }

    override fun isLogin(): Boolean {
        return login
    }

    override fun getUserId(): String? {
        return userId
    }
}

5.3 在其他模块中通过路由获取服务

比如在 module_home 中,你想判断用户是否登录:

kotlin 复制代码
val userService = ARouter.getInstance()
    .build("/service/user")
    .navigation() as? IUserService

if (userService?.isLogin() == true) {
    // 已登录
    val uid = userService.getUserId()
    Toast.makeText(this, "当前用户ID:$uid", Toast.LENGTH_SHORT).show()
} else {
    // 未登录,跳到登录页
    ARouter.getInstance()
        .build("/user/login")
        .navigation()
}

这样,Home 模块并不知道 User 模块内部的实现细节,只是通过 /service/user 这个路径找到了一个 IUserService 实例,实现了 逻辑层面的组件通信


六、组件"独立运行"和"集成调试"的切换

真正做组件化时,有很重要的 一点是模块在开发阶段希望能单独作为 App 启动调试 ,在最终打包时又集成到统一的 app 中。

AndroidStudio创建一个Android项目后,会在根目录中生成一个gradle.properties文件。在这个文件定义的常量,可以被任何一个build.gradle读取。 所以我们可以在gradle.properties中定义一个常量值 isModule,true为即独立调试;false为集成调试。然后在业务组件的build.gradle中读取 isModule即可。

同时每个组件在独立调试时也是一个App都需要一个 ApplicationId和一个启动页,而启动页是在AndroidManifest.xml中设置的,所以ApplicationIdAndroidManifest也是需要 isModule 来进行配置

6.1 在 gradle.properties 中定义开关

properties 复制代码
IS_HOME_MODULE_DEBUG=true
IS_USER_MODULE_DEBUG=false

6.2 在对应模块的 build.gradle 中根据开关配置

module_home 举例:

groovy 复制代码
def isModuleDebug = project.hasProperty("IS_HOME_MODULE_DEBUG") &&
        IS_HOME_MODULE_DEBUG.toBoolean()

android {
    defaultConfig {
        if (isModuleDebug) {
            // 单独调试时需要应用 ID
            applicationId "com.example.module_home"
        }
    }

    sourceSets {
        main {
            if (isModuleDebug) {
                // 独立运行时使用 module_home 自己的 manifest
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // 集成到 app 时使用 library 的 manifest
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

if (isModuleDebug) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

同时在新建包下准备单独调试的AndroidManifest,这里以module_home为例

xml 复制代码
//moduleManifest/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfy.module_home" >
    <application android:name=".HomeApplication"
        android:allowBackup="true"
        android:label="Home"
        android:theme="@style/Theme.AppCompat">
        <activity android:name=".HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

这样就能做到IS_HOME_MODULE_DEBUG=true 时,module_home 可以作为一个单独 App 编译运行(方便只调首页相关功能),在改成 false 后,就恢复成一个普通 library,由 app 统一壳工程来启动。

相关推荐
技术摆渡人1 小时前
Android 系统技术探索(2)构建大脑(System Services & PMS)
android
tealcwu1 小时前
【Unity实战】如何使用VS Code在真实Android设备上调试 Unity应用
android·unity·游戏引擎
鹏多多2 小时前
flutter-屏幕自适应插件flutter_screenutil教程全指南
android·前端·flutter
小龙报2 小时前
【C语言初阶】动态内存分配实战指南:C 语言 4 大函数使用 + 经典笔试题 + 柔性数组优势与内存区域
android·c语言·开发语言·数据结构·c++·算法·visual studio
小龙报2 小时前
【算法通关指南:算法基础篇(三)】一维差分专题:1.【模板】差分 2.海底高铁
android·c语言·数据结构·c++·算法·leetcode·visual studio
TeleostNaCl2 小时前
使用 Android Jetpack 中的 Startup 组件快速实现组件初始化逻辑与主模块解耦
android·经验分享·android jetpack·androidx·android runtime·jetpack android
恋猫de小郭2 小时前
让 AI 用 Flutter 实现了猗窝座的破坏杀·罗针动画,这个过程如何驯服 AI
android·前端·flutter
2501_9371931412 小时前
技术加持!PLB-TV:HDR10+UDP 传输
android·源码·源代码管理·机顶盒
霸王大陆14 小时前
《零基础学 PHP:从入门到实战》模块十:从应用到精通——掌握PHP进阶技术与现代化开发实战-2
android·开发语言·php