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 统一壳工程来启动。

相关推荐
00后程序员张21 小时前
iOS 应用程序使用历史记录和耗能记录怎么查?
android·ios·小程序·https·uni-app·iphone·webview
用户69371750013841 天前
OS级AI Agent:手机操作系统的下一个战场
android·前端·人工智能
私人珍藏库1 天前
[Android] 亿连车机版V7.0.1
android·app·软件·车机
用户69371750013841 天前
315曝光AI搜索问题:GEO技术靠内容投喂操控答案,新型营销操作全揭秘
android·前端·人工智能
进击的cc1 天前
彻底搞懂 Binder:不止是 IPC,更是 Android 的灵魂
android·面试
段娇娇1 天前
Android jetpack LiveData (三) 粘性数据(数据倒灌)问题分析及解决方案
android·android jetpack
用户2018792831671 天前
TabLayout被ViewPager2遮盖部分导致Tab难选中
android
法欧特斯卡雷特1 天前
Kotlin 2.3.20 现已发布,来看看!
android·前端·后端
闻哥1 天前
深入理解 MySQL InnoDB Buffer Pool 的 LRU 冷热数据机制
android·java·jvm·spring boot·mysql·adb·面试
ii_best1 天前
安卓/ios开发辅助软件按键精灵小精灵实现简单的UI多配置管理
android·ui·ios·自动化