第2章 第三方SDK接口适配层设计:抽象接口定义、回调适配、线程切换封装

接入多个第三方 SDK 时,各厂商提供的 API 形态、回调机制、线程模型差异很大。适配层通过抽象接口封装将异构接口统一,通过回调桥接将原生回调转为 Kotlin 协程或 Flow,通过线程切换封装确保 UI 更新发生在主线程。本章围绕这三块核心设计展开。

接口适配层架构 Compose UI 应用层 适配层 抽象接口定义 回调桥接 线程切换封装 SDK A SDK B SDK C 适配层向上暴露统一接口,向下对接多个厂商SDK,隔离差异,降低替换成本

2.1 抽象接口定义

定义应用层不依赖具体 SDK 的功能接口,例如登录、支付、日志上报。接口中所有耗时任务返回 suspend 函数或 Flow,屏蔽回调。

核心接口示例:
interface AuthService {
    suspend fun login(account: String, password: String): Result<UserInfo>
    fun observeLoginState(): StateFlow<LoginState>
}

interface PaymentService {
    suspend fun pay(order: Order): Result<PayResult>
    fun observePayResult(): SharedFlow<PayEvent>
}
设计要点: 接口方法应正交——每个方法只做一件事;参数尽量使用领域模型而非原生字典;返回值使用 Result<T> 包装错误,避免底层异常透传。

2.2 回调适配

第三方 SDK 通常以 Listener/Callback 形式返回结果。适配层使用 suspendCancellableCoroutine 或 callbackFlow 将其转化为协程。

// 以登录回调为例
class SdkALoginAdapter(private val sdk: SdkALoginApi) : AuthService {
    override suspend fun login(account: String, password: String): Result<UserInfo> =
        suspendCancellableCoroutine { continuation ->
            sdk.login(account, password, object : SdkALoginCallback {
                override fun onSuccess(user: UserInfo) {
                    continuation.resume(Result.success(user))
                }
                override fun onError(code: Int, msg: String) {
                    continuation.resume(Result.failure(SdkException(code, msg)))
                }
            })
            continuation.invokeOnCancellation {
                sdk.cancelLogin()
            }
        }
}
注意: 部分 SDK 的回调可能多次触发(如进度回调)。对于一次性结果使用 suspendCancellableCoroutine;对于多次事件使用 callbackFlow 构造 SharedFlow,并在 awaitClose 中反注册。

2.3 线程切换封装

第三方回调几乎都在其内部线程池执行,若直接更新 Compose 状态会触发 CalledFromWrongThreadException。适配层统一通过 withContext(Dispatchers.Main) 将结果投递到主线程。

// 在线程安全层中统一切换
class SafeAuthService(private val delegate: AuthService) : AuthService {
    override suspend fun login(account: String, password: String): Result<UserInfo> =
        withContext(Dispatchers.IO) {
            delegate.login(account, password)
        }.also { result ->
            // 确保回到主线程
            withContext(Dispatchers.Main.immediate) {
                // 这里已经处于主线程,可以直接更新 StateFlow
            }
        }

    override fun observeLoginState(): StateFlow<LoginState> {
        val original = delegate.observeLoginState()
        return original.map { state ->
            // 因为 StateFlow collect 默认在协程上下文,不需要额外切换
            state
        }.stateIn(
            scope = CoroutineScope(Dispatchers.Main),
            started = SharingStarted.WhileSubscribed(),
            initialValue = LoginState.Idle
        )
    }
}

2.4 调试方法

调试场景方法验证手段
回调线程检查在回调桥接位置打印 Thread.currentThread().name确认非 main 线程
状态更新验证Compose 中加 @Stable 标记,使用 snapshotFlow 打印UI 重组次数符合预期
内存泄漏检测LeakCanary 配合 Compose 测试退出页面后无 Activity/Fragment 引用残留
协程挂起耗时在 suspendCancellableCoroutine 前后加计时日志SDK 响应时间

2.5 部署集成

将适配层及第三方 SDK 封装为独立 Gradle 模块(如 :sdk-adapters),对外只暴露抽象接口和工厂方法。应用模块通过依赖注入获取具体实现。

// app/build.gradle.kts
dependencies {
    implementation(project(":sdk-adapters"))
    // 具体厂商SDK只由 adapters 模块依赖,app 不需直接引用
}
最佳实践: 使用 Hilt/Dagger 在适配层提供 @Binds 绑定,或者使用 ServiceLoader 方式动态加载。这样更换 SDK 时只需替换适配模块,应用代码零改动。

2.6 常见错误

  • 忘记线程切换:直接更新 mutableStateOf 导致崩溃。解决办法:所有更新 Compose 状态的操作必须发生在 Dispatchers.Main。
  • 回调多次注册:callbackFlow 未在 awaitClose 中反注册,导致内存泄漏。必须在协程取消时调用 SDK 的 removeListener。
  • Result 误用:将 Result.success 传入异常场景;建议在适配层统一转换为自定义密封类。
  • StateFlow 初始值不一致:stateIn 的初始值与真实状态不同步,导致 UI 闪跳。初始化时应从 SDK 获取一次当前状态。

2.7 性能优化

  • 共享流复用:对重复订阅的观察者使用 SharedFlow(replay=1) 减少创建开销;配合 WhileSubscribed() 策略,在无订阅时自动释放。
  • 避免过度线程切换:若回调本身已处于主线程(部分 SDK 可配置),适配层应跳过 withContext,通过环境配置标志位控制。
  • 协程作用域划分:每个页面或业务模块持有独立的 CoroutineScope,避免全局作用域导致后台任务堆积。
  • 对象池:针对高频回调(如位置更新),在适配层复用包装对象,减少 GC 压力。
// 性能优化示例:使用 SharingStarted.WhileSubscribed 自动释放
val loginState: StateFlow<LoginState> = delegate.observeLoginState()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
        initialValue = LoginState.Idle
    )
坑点提醒: WhileSubscribed(stopTimeoutMillis) 设为 0 时,每次订阅 / 取消都会触发重新启动冷流,导致重复初始化 SDK。建议保留 3~5 秒超时。