组件间通信
问题
模块化后各模块之间如何通信?如何在不产生直接依赖的情况下互相调用?
答案
通信挑战
模块化后 feature-home 和 feature-profile 不能互相依赖,但业务需要互相跳转和数据交换:
app
├── feature-home ←→ feature-profile ❌ 不能互相依赖
├── feature-profile
├── core-common ← 两者都可以依赖的公共层
└── core-navigation
方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 接口下沉 | 接口定义在公共模块 | 类型安全、IDE 友好 | 公共模块膨胀 |
| SPI / ServiceLoader | Java SPI 机制 | 无需公共接口模块 | 反射开销 |
| 路由框架 | URL 映射 | 解耦彻底 | 字符串硬编码 |
| 事件总线 | 发布-订阅 | 简单 | 难以追踪、类型不安全 |
| 依赖注入 | DI 容器管理 | 类型安全、可测试 | 配置复杂 |
方案一:接口下沉(推荐)
core-common/IUserService.kt
// 接口定义在公共模块
interface IUserService {
fun getUserName(): String
fun navigateToProfile(context: Context, userId: String)
}
feature-profile/UserServiceImpl.kt
// 实现在各自模块
class UserServiceImpl : IUserService {
override fun getUserName() = "Vincken"
override fun navigateToProfile(context: Context, userId: String) {
context.startActivity(Intent(context, ProfileActivity::class.java).apply {
putExtra("userId", userId)
})
}
}
app 模块中用 Hilt 绑定
@Module
@InstallIn(SingletonComponent::class)
abstract class ServiceModule {
@Binds
abstract fun bindUserService(impl: UserServiceImpl): IUserService
}
feature-home 使用
@HiltViewModel
class HomeViewModel @Inject constructor(
private val userService: IUserService // 只依赖接口
) : ViewModel()
方案二:SPI / ServiceLoader
core-common/IFeatureService.kt
interface IFeatureService {
fun init(context: Context)
}
feature-home/HomeFeatureService.kt
class HomeFeatureService : IFeatureService {
override fun init(context: Context) { /* ... */ }
}
feature-home/src/main/resources/META-INF/services/com.example.IFeatureService
com.example.home.HomeFeatureService
加载服务
val services = ServiceLoader.load(IFeatureService::class.java)
services.forEach { it.init(context) }
常见面试问题
Q1: 接口下沉方案会导致公共模块膨胀吗?如何解决?
答案:
当接口越多,公共模块确实会变大。解决方案是进一步拆分公共层:
core-common # 基础工具
core-model # 数据模型
core-service-api # 服务接口定义
├── user-api
├── order-api
└── payment-api
或者每个 feature 模块拆分出一个 -api 模块:feature-profile-api(只包含接口),其他模块依赖 -api 而不依赖实现模块。
Q2: 组件化中各模块如何独立运行调试?
答案:
通过 isModule 开关将 feature 模块在 application 和 library 之间切换:
// feature-home/build.gradle.kts
val isModule: Boolean = project.findProperty("isModule")?.toString()?.toBoolean() ?: false
if (isModule) {
apply(plugin = "com.android.application")
} else {
apply(plugin = "com.android.library")
}
独立运行时提供单独的 AndroidManifest.xml(包含 LAUNCHER Activity)和 Application。