安卓开发中,在项目做大之后,一个 超级
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_home、module_user、module_mall,但尽量在该 模块中少写业务代码,只负责入口和整体导航。2. 做全局初始化,比如:ARouter 初始化、网络库初始化、日志、Crash、埋点等以及全局 Theme、字体、语言的处理。维护"真正的" Application 和启动入口
-
common一般作为基础工具模块,包含- 基础工具类:网络封装、日志封装、图片加载封装等
- 一些全局常量、基础 UI 组件(
BaseActivity、BaseFragment) - 路由常量(例如
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(新版一般不用,直接在模块中引依赖即可,这里以常用写法为例),**在各个需要使用路由的模块(如 app、module_home、module_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中有一个HomeActivitymodule_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中设置的,所以ApplicationId、AndroidManifest也是需要 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 统一壳工程来启动。