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的核心原理

  1. 编译期注解处理:通过APT(Annotation Processing Tool)在编译时扫描所有加了@Route注解的类,生成路由表文件
  2. 运行时加载:App启动时,ARouter会加载这些路由表,建立路径到类的映射关系
  3. 拦截器机制:支持在路由跳转前后插入自定义逻辑,比如登录检查、埋点等

关键点: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:成熟的解决方案,支持路由跳转、服务注入、拦截器

最后分享几个我个人的习惯:

  1. 接口定义放在base模块,实现放在各自组件
  2. 路径字符串用常量管理,不要硬编码
  3. 拦截器优先级要规划好,比如登录拦截器优先级最高
  4. 记得在Application中初始化ARouter,并开启日志方便调试

组件通信这块,说白了就是「约定大于配置」。只要大家遵守统一的接口规范和路径规范,组件化开发就会顺畅很多。

一句话总结:接口定义规范,服务发现解耦,ARouter落地执行。这三板斧用好了,组件通信就不再是难题。