4、组件通信方案:接口+实现分离,服务发现机制,阿里ARouter原理与实战
组件化开发中,最头疼的问题是什么?
我个人觉得,就是组件之间的通信。你想啊,每个组件都是独立的,互相不依赖。那A组件怎么调用B组件的功能?总不能直接new一个对象吧?那不就耦合了吗?
嗯,今天我们就来聊聊这个核心问题。我会结合我在几个大型项目中的实际经验,把接口+实现分离、服务发现机制,还有ARouter的原理和用法,一次性讲透。
4.1 组件通信的痛点:为什么不能直接调用?
先说说我踩过的坑。几年前我做一个电商App,订单组件和支付组件是分开的。订单页面需要调起支付,我当时图省事,直接在订单模块里写了PaymentActivity.start()。结果呢?
后来支付组件要重构,包名改了。订单模块那边编译直接报错,整个项目都得跟着改。这就是典型的强耦合。
组件化通信要解决的核心问题就两个:
- 解耦:组件之间不能有直接的类依赖
- 动态性:运行时才能确定目标组件是否存在
核心原则:组件之间只依赖接口,不依赖实现。接口定义在公共的base层,实现放在各自的组件里。
4.2 接口+实现分离:最基础的解耦方案
说白了,就是定义一个接口,然后让各个组件去实现它。调用方只认接口,不认具体类。
举个例子。假设有一个用户信息组件,需要对外提供获取用户昵称的功能。
第一步:在base层定义接口
// 放在 base 模块中
interface IUserService {
fun getUserName(): String
fun isLoggedIn(): Boolean
}
第二步:在user组件中实现接口
// 放在 user 模块中
class UserServiceImpl : IUserService {
override fun getUserName(): String {
return SPUtils.getString("user_name", "未登录")
}
override fun isLoggedIn(): Boolean {
return SPUtils.getBoolean("is_login", false)
}
}
第三步:调用方通过接口调用
// 放在 order 模块中
class OrderFragment : Fragment() {
fun showUserInfo() {
// 这里只依赖 IUserService 接口
val userService: IUserService = ???? // 问题来了:怎么拿到实现?
}
}
看到没?问题来了。调用方只知道接口,但不知道去哪里找实现类。这就是「服务发现」要解决的问题。
我的经验:接口+实现分离是基础,但光有它还不够。你还需要一个「中介」来管理接口和实现之间的映射关系。这个中介,就是服务发现机制。
4.3 服务发现机制:让组件互相找到对方
服务发现,说白了就是一个注册中心。各个组件在启动时把自己的实现类注册进去,调用方通过接口名去查找。
我早期自己写过一个简单的服务发现框架,核心代码也就几十行:
object ServiceManager {
private val services = mutableMapOf<Class<*>, Any>()
fun <T> register(serviceClass: Class<T>, impl: T) {
services[serviceClass] = impl
}
@Suppress("UNCHECKED_CAST")
fun <T> getService(serviceClass: Class<T>): T? {
return services[serviceClass] as? T
}
}
使用方式也很简单:
// 在 Application 中注册
ServiceManager.register(IUserService::class.java, UserServiceImpl())
// 在任意地方获取
val userService = ServiceManager.getService(IUserService::class.java)
userService?.let {
textView.text = it.getUserName()
}
这个方案虽然简单,但有几个问题:
- 注册时机要把握好,必须在调用之前完成
- 不支持跨进程通信
- 没有参数传递能力
所以,在实际项目中,我们一般会用更成熟的方案——ARouter。
4.4 阿里ARouter原理:它到底做了什么?
ARouter是阿里开源的Android路由框架。很多人以为它只是用来跳转Activity的,其实它的核心能力是「服务发现」+「路由跳转」。
ARouter的核心原理:
- 编译期注解处理:通过APT(Annotation Processing Tool)在编译时扫描所有加了
@Route注解的类,生成路由表文件 - 运行时加载:App启动时,ARouter会加载这些路由表,建立路径到类的映射关系
- 拦截器机制:支持在路由跳转前后插入自定义逻辑,比如登录检查、埋点等
关键点:ARouter的路径是字符串,不是类引用。这就彻底解除了编译期的依赖。你想想看,只要路径字符串不变,目标类怎么改都不会影响调用方。
4.5 ARouter实战:服务发现与依赖注入
ARouter不仅支持页面跳转,还支持服务发现。它通过@Autowired注解实现类似Dagger的依赖注入效果。
第一步:添加依赖
// 在 build.gradle 中
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
implementation 'com.alibaba:arouter-api:1.5.2'
annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}
第二步:定义并注册服务接口
// 接口定义在 base 模块
interface IUserService {
fun getUserName(): String
}
// 实现类在 user 模块,用 @Route 注解标记
@Route(path = "/user/service")
class UserServiceImpl : IUserService {
override fun getUserName(): String {
return "张三"
}
}
第三步:在调用方注入服务
class OrderActivity : AppCompatActivity() {
@Autowired(name = "/user/service")
lateinit var userService: IUserService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this) // 必须调用 inject
// 直接使用
textView.text = userService.getUserName()
}
}
你看,整个过程调用方完全不知道UserServiceImpl这个类存在。这就是解耦。
我曾经踩过的坑:ARouter的inject()方法必须在super.onCreate()之后调用,否则注入会失败。另外,如果服务实现类所在的模块没有被编译到APK中,运行时就会报NoClassDefFoundError。所以记得在app模块的dependencies里加上所有组件模块的依赖。
4.6 ARouter页面跳转:不仅仅是startActivity
除了服务发现,ARouter最常用的还是页面跳转。它比原生的Intent强在哪?
| 功能 | 原生Intent | ARouter |
|---|---|---|
| 跨模块跳转 | 需要类依赖 | 字符串路径,无依赖 |
| 参数传递 | Bundle,类型不安全 | 自动解析,支持基本类型和序列化 |
| 拦截器 | 不支持 | 支持,可做登录校验等 |
| 降级策略 | try-catch | 全局降级回调 |
来个实际例子:
// 在订单模块中跳转到支付模块的页面
ARouter.getInstance()
.build("/payment/pay") // 路径
.withString("order_id", "12345") // 传参
.withInt("amount", 9900) // 金额,单位分
.navigation(this) // 执行跳转
// 在支付模块的 Activity 中接收参数
@Route(path = "/payment/pay")
class PayActivity : AppCompatActivity() {
@Autowired
lateinit var orderId: String
@Autowired(name = "amount")
var payAmount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
// 直接使用 orderId 和 payAmount
}
}
我建议:路径命名要有规范。比如统一用/模块名/功能名的格式。我在项目中就吃过路径混乱的亏,后来强制要求所有路径写在同一个常量类里管理。
4.7 拦截器:让路由更智能
ARouter的拦截器机制非常实用。比如用户未登录时跳转到支付页面,应该先跳转到登录页。
@Interceptor(priority = 1) // priority 越小优先级越高
class LoginInterceptor : IInterceptor {
override fun process(postcard: Postcard, callback: InterceptorCallback) {
if (postcard.path.startsWith("/payment/")) {
// 检查是否登录
if (!UserManager.isLoggedIn()) {
// 中断当前路由,跳转到登录页
ARouter.getInstance()
.build("/user/login")
.withString("target_path", postcard.path)
.navigation()
callback.onInterrupt(null)
return
}
}
callback.onContinue(postcard)
}
override fun init(context: Context) {
// 初始化操作
}
}
这样做的好处是,所有支付相关的页面都不需要自己写登录检查逻辑。拦截器统一处理,干净利落。
4.8 总结与最佳实践
好了,我们来捋一捋今天的内容:
- 接口+实现分离:基础中的基础,让组件只依赖接口
- 服务发现机制:通过注册中心管理接口和实现的映射
- ARouter:成熟的解决方案,支持路由跳转、服务注入、拦截器
最后分享几个我个人的习惯:
- 接口定义放在base模块,实现放在各自组件
- 路径字符串用常量管理,不要硬编码
- 拦截器优先级要规划好,比如登录拦截器优先级最高
- 记得在Application中初始化ARouter,并开启日志方便调试
组件通信这块,说白了就是「约定大于配置」。只要大家遵守统一的接口规范和路径规范,组件化开发就会顺畅很多。
一句话总结:接口定义规范,服务发现解耦,ARouter落地执行。这三板斧用好了,组件通信就不再是难题。