首页 / 资讯中心 / 文章详情

Retrofit自定义CallAdapter实战:从Result封装到协程Flow交付

Retrofit自定义CallAdapter实战:从Result封装到协程Flow交付 ★ FEATURED ARTICLE
1. 项目概述为什么自定义 CallAdapter 是 Retrofit 真正的“开关”你写过 Retrofit 的接口也用过CallT和LiveDataT甚至可能在项目里混着用RxJava2CallAdapterFactory和CoroutineCallAdapterFactory。但有没有哪一刻你盯着GET(user) suspend fun getUser(): User这行代码发愣——它到底怎么从网络响应变成一个挂起函数的又或者当你想把某个接口的返回统一包装成ResultT比如ResultUser却不得不在每个onResponse()里手动套一层手酸到想砸键盘这些不是使用问题而是 Retrofit 的“能力边界”没被你真正打开。CallAdapter 就是那个边界开关——它不处理网络请求也不解析 JSON但它决定了“请求完成之后结果该以什么形态交到你手上”。Retrofit 默认只认CallT但你的业务需要FlowT、DeferredT、StateFlowApiResponseT甚至是一个带重试逻辑的SmartCallT这时候CallAdapter就不再是源码阅读的“选修课”而是你掌控整个网络层抽象能力的必经之路。我带过的几个团队里凡是能把自定义CallAdapter写明白的 Android 开发者基本不会再被“回调嵌套”“线程切换混乱”“统一错误拦截难”这类问题卡住而那些只停留在addCallAdapterFactory(RxJava2CallAdapterFactory.create())复制粘贴阶段的往往在做中台 SDK 或跨模块通信时突然发现网络层像一锅粥——改一个地方十个地方报红。这不是 Retrofit 设计得不好而是你没拿到它的“控制台权限”。这篇内容就是帮你把这把钥匙真正攥进手里不讲泛泛的“适配器模式”不堆砌源码截图而是从你每天写的那行suspend fun出发一层层拆开CallAdapter的骨架、血肉和神经末梢告诉你它怎么被创建、怎么被调用、怎么和Converter协同、怎么安全地切线程、怎么优雅地处理异常以及——最关键的是为什么你写的MyCustomCallAdapter在某些场景下会静默失败而另一些时候又莫名其妙多执行了一次。适合谁如果你能独立写出一个 MVP 架构的登录页但看到CallAdapter.Factory的get()方法签名就头皮发紧如果你用协程但总在viewModelScope.launch里手动try/catch网络异常如果你的 App 里同时存在LiveData、Flow、Callback三种回调风格维护成本越来越高——那这篇就是为你写的。它不假设你熟读 OkHttp 源码但要求你写过至少 3 个 Retrofit 接口并被enqueue()的onFailure()里那个Throwable折磨过。2. 核心设计思路CallAdapter 不是“转换器”而是“交付协议”2.1 从 Retrofit 的方法签名反推设计意图先看一个最基础的 Retrofit 接口定义interface ApiService { GET(user/{id}) fun getUser(Path(id) id: Int): CallUser }这里CallUser是 Retrofit 唯一原生支持的返回类型。但当你换成GET(user/{id}) suspend fun getUser(Path(id) id: Int): UserRetrofit 并没有报错反而能正常工作。为什么因为suspend函数在编译后会被 Kotlin 编译器转成ContinuationUser而 Retrofit 的RequestFactory在解析方法签名时会检查返回类型是否实现了CallAdapter的适配规则。关键点在于Retrofit 本身并不关心你返回的是Call、Flow还是suspend它只关心“有没有一个 CallAdapter 能接住这个类型并承诺把它变成 Call 或直接执行网络请求”。这就是CallAdapter的本质——它不是数据格式转换器那是Converter的事而是一份交付协议你告诉我你要什么形态的结果T我负责按约定把网络响应、异常、取消信号打包成你指定的容器并确保生命周期、线程、错误传播都符合你的预期。2.2 CallAdapter 的三层职责模型我把CallAdapter的实际工作拆成三个不可分割的层次这是理解所有自定义行为的基础第一层类型声明与契约绑定CallAdapter的responseType()方法必须返回User即T而不是FlowUser或DeferredUser。这意味着CallAdapter的核心任务之一是“剥掉外层容器暴露真实业务数据类型”。比如FlowCallAdapter的responseType()返回User而它自己内部管理FlowUser的创建和发射。这个设计强制你明确区分“数据载体”和“数据本体”避免像早期某些封装库那样把LiveDataUser当作T导致泛型擦除后无法正确解析 JSON。第二层执行时机与生命周期托管CallAdapter的adapt()方法接收一个原始的CallT但返回的却可能是FlowT或DeferredT。这里的关键是adapt()不执行网络请求它只是“注册监听”或“准备执行”。真正的请求触发发生在你调用flow.collect{}或deferred.await()的那一刻。Retrofit 把执行权完全交给你——Flow的collect启动请求Deferred的await启动请求suspend fun的调用本身启动请求。这种设计让CallAdapter能天然支持懒加载、多次收集、取消传播等高级特性但也意味着你必须在adapt()中正确处理Call.cancel()与Flow.cancellation的映射否则会出现“界面退出后请求还在后台跑”的经典问题。第三层错误与状态的语义化封装默认CallT只提供onResponse()和onFailure()错误信息藏在Throwable里。但业务上你需要区分“网络超时”“服务器返回 500”“JSON 解析失败”“Token 过期需跳登录”。CallAdapter允许你在adapt()中注入统一的状态处理器。比如ResultCallAdapter会把所有异常包装成Result.failure(throwable)而成功响应包装成Result.success(data)这样 ViewModel 层拿到的永远是ResultUser无需每个地方if (it.isSuccessful) ... else ...。但注意这种封装不能破坏 Retrofit 的异常传播链。如果Converter解析失败抛出JsonParseExceptionCallAdapter必须确保这个异常能被上层捕获而不是静默吞掉——否则你会陷入“接口明明失败了但 Flow 就是不发射任何东西”的调试地狱。2.3 为什么不能只靠 Converter 解决所有问题常有开发者问“我能不能在Converter里直接返回ResultT然后CallResultT就完事了” 答案是技术上可行但逻辑上灾难。Converter的职责是数据格式转换它只管ResponseBody→T这一段。而ResultT的failure分支根本不是从ResponseBody来的——它可能来自网络层的IOException可能来自Call.cancel()可能来自Interceptor主动抛出的异常。Converter没有上下文感知能力它不知道当前请求是否被取消也不知道OkHttpClient是否配置了超时。把这些逻辑塞进Converter等于让一个“翻译官”去干“项目经理”的活既要翻译合同JSON又要协调工期超时、处理违约取消、应对政策变化Token 刷新。CallAdapter才是那个项目经理它站在Call和最终返回类型之间统筹所有非数据转换的流程控制。这也是为什么 Retrofit 官方文档强调“CallAdapteris responsible for adapting theCallto the desired return type, whileConverterhandles the data conversion.” —— 两个角色泾渭分明。3. 自定义 CallAdapter 的实操实现从零手写一个 ResultCallAdapter3.1 工程结构与依赖确认在动手前请确认你的项目已引入以下依赖以 Retrofit 2.9.0 OkHttp 4.12.0 为例implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.okhttp3:okhttp:4.12.0 // 如果要用协程还需添加 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation androidx.lifecycle:lifecycle-runtime-ktx:2.7.0注意CallAdapter的实现与Converter无关但你的ResultT需要能被 Gson 正确解析所以GsonConverterFactory仍是必需的。另外不要在build.gradle中添加retrofit2-kotlin-coroutines-adapter我们要从零实现才能看清每一步的取舍。3.2 核心类定义与 Factory 模式首先定义ResultCallAdapter类它必须继承CallAdapterResultT, CallT注意泛型顺序CallAdapterR, T中R是返回类型T是响应类型class ResultCallAdapterT(private val responseType: Type) : CallAdapterResultT, CallT { override fun responseType(): Type responseType override fun adapt(call: CallT): ResultT { // 这里是核心逻辑稍后展开 TODO(Not yet implemented) } }接着实现Factory这是 Retrofit 发现并加载CallAdapter的入口class ResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: ArrayAnnotation, retrofit: Retrofit ): CallAdapter*, *? { // 1. 检查返回类型是否为 Result* if (getRawType(returnType) ! Result::class.java) { return null } // 2. 提取 Result 的泛型参数即 T val resultType getParameterUpperBound(0, returnType as ParameterizedType) // 3. 创建并返回 ResultCallAdapter 实例 return ResultCallAdapterAny(resultType) as CallAdapter*, * } }这里有两个关键技巧getRawType(returnType)用于获取ResultUser的原始类Result避免因泛型擦除无法匹配getParameterUpperBound(0, ...)用于提取ResultUser中的User类型这是后续Converter解析 JSON 的依据。Retrofit 内部大量使用Utils工具类处理泛型我们复用它保证兼容性。3.3 adapt() 方法的完整实现线程、异常、取消的三重编织现在进入最核心的adapt()方法。注意这里不能直接执行网络请求也不能阻塞主线程。我们的目标是返回一个ResultT但Result是密封类无法直接实例化。因此我们必须返回一个“可计算出 Result 的对象”而Result本身是不可变的所以实际返回的是ResultT的生产者——一个suspend fun或CallableResultT。但 Retrofit 要求adapt()返回ResultT这就矛盾了。解决方案是返回一个惰性计算的 Result其内部封装了 Call 的执行逻辑。标准做法是返回一个ResultT的包装类但我们选择更简洁的方式——利用 Kotlin 的runCatchingoverride fun adapt(call: CallT): ResultT { return try { val response call.execute() // 同步执行仅用于演示实际应避免 if (response.isSuccessful) { val body response.body() Result.success(body!!) } else { Result.failure(HttpException(response)) } } catch (e: Throwable) { Result.failure(e) } }但这段代码有严重缺陷call.execute()是同步阻塞调用会卡死主线程。真实项目中我们必须异步执行。Retrofit 的CallAdapter设计允许你返回任何类型只要它能“代表”最终结果。因此更合理的方案是返回一个DeferredResultT或FlowResultT。我们选择Flow因为它天然支持取消、多次收集、生命周期绑定class ResultCallAdapterT(private val responseType: Type) : CallAdapterFlowResultT, CallT { override fun responseType(): Type responseType override fun adapt(call: CallT): FlowResultT { return flow { try { val response call.execute() emit( if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(HttpException(response)) } ) } catch (e: Throwable) { emit(Result.failure(e)) } }.flowOn(Dispatchers.IO) // 确保 execute() 在 IO 线程执行 } }但这里仍有隐患call.execute()在flow的try块中执行如果Flow被取消如collect作用域结束call不会自动取消导致请求继续在后台运行。必须显式绑定取消override fun adapt(call: CallT): FlowResultT { return callbackFlow { val callback object : CallbackT { override fun onResponse(call: CallT, response: ResponseT) { trySend( if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(HttpException(response)) } ) close() // Flow 收集完成关闭通道 } override fun onFailure(call: CallT, t: Throwable) { trySend(Result.failure(t)) close() } } call.enqueue(callback) // 使用 enqueue非阻塞 awaitClose { call.cancel() } // Flow 取消时主动 cancel Call }.flowOn(Dispatchers.IO) }这段代码解决了三大问题使用enqueue()替代execute()避免主线程阻塞callbackFlow提供awaitClose确保Flow生命周期与Call绑定trySend保证线程安全close()确保 Flow 正常结束。3.4 Factory 的增强支持泛型约束与注解驱动上面的ResultCallAdapterFactory只检查了returnType是否为Result但实际业务中你可能需要区分“是否需要自动刷新 Token”或“是否启用本地缓存”。这时可以利用 Retrofit 的annotations参数。例如定义一个AutoRefresh注解Retention(AnnotationRetention.RUNTIME) Target(AnnotationTarget.FUNCTION) annotation class AutoRefresh然后在Factory.get()中解析override fun get( returnType: Type, annotations: ArrayAnnotation, retrofit: Retrofit ): CallAdapter*, *? { if (getRawType(returnType) ! Result::class.java) return null val resultType getParameterUpperBound(0, returnType as ParameterizedType) val autoRefresh annotations.find { it is AutoRefresh } ! null return ResultCallAdapter(resultType, autoRefresh) }这样你就可以在接口中精准控制AutoRefresh GET(user/{id}) fun getUser(Path(id) id: Int): ResultUser而其他接口保持默认行为。这种基于注解的动态适配是CallAdapter灵活性的集中体现也是它比简单封装工具类更强大的原因——它把策略决策权交还给接口定义本身而非全局配置。4. 深度实操与 Converter、Interceptor 的协同作战4.1 CallAdapter 与 Converter 的调用时序图解很多开发者混淆CallAdapter和Converter的执行顺序。用一个真实请求链来说明你调用apiService.getUser(123).collect{}ResultCallAdapter.adapt()被触发内部调用call.enqueue(callback)OkHttp 发起网络请求收到ResponseBodyRetrofit 调用Converter.convert(responseBody, responseType)将字节流转为User对象Converter返回UserCallback.onResponse()被调用ResultCallAdapter的onResponse()回调中emit(Result.success(user))。关键点Converter在CallAdapter的onResponse()之前执行且只对成功响应生效。如果Converter解析失败如 JSON 字段缺失它会抛出RuntimeException这个异常会直接传递给Callback.onFailure()进而被ResultCallAdapter包装成Result.failure(exception)。因此Converter的健壮性直接影响ResultT的failure分支内容。建议在自定义Converter中对常见解析异常做分类class SafeGsonConverterT(private val gson: Gson, private val type: Type) : ConverterResponseBody, T { override fun convert(value: ResponseBody): T? { return try { gson.fromJson(value.charStream(), type) } catch (e: JsonParseException) { throw DataParseException(JSON parse error, e) // 自定义异常便于 Result 区分 } catch (e: IOException) { throw NetworkIOException(IO error, e) } } }这样Result.failure()中的Throwable就携带了语义化类型ViewModel 层可针对性处理when (result) { is Result.Success - showUser(result.data) is Result.Failure - when (result.exception) { is DataParseException - showToast(数据格式异常) is NetworkIOException - retryButton.visibility View.VISIBLE else - navigateToLogin() } }4.2 Interceptor 如何影响 CallAdapter 的行为Interceptor运行在 OkHttp 层它能看到并修改Request和Response但对CallAdapter是透明的。然而它的行为会间接改变CallAdapter的输入。典型场景Token 刷新拦截器当收到 401 响应时拦截器自动刷新 Token 并重试请求。此时CallAdapter的onResponse()会收到重试后的成功响应Result.success()包含的是新数据而非原始 401。这对ResultCallAdapter是友好的因为它不关心中间过程。日志拦截器打印RequestBody和ResponseBody。如果ResponseBody被Interceptor缓存或修改如添加Content-Encoding: gzipConverter必须能正确解压否则Result.failure()会包含IOException。最危险的是修改响应码的拦截器。例如某拦截器将所有 5xx 响应统一改为 200并在 Body 中添加code:500字段。此时Converter解析出的User对象可能为空而CallAdapter仍会走onResponse()分支返回Result.success(null)导致 NPE。正确做法是在 Interceptor 中保持 HTTP 语义不变业务错误码应放在 Response Body 内由 Converter 或 CallAdapter 统一处理。4.3 线程切换的黄金法则谁创建谁调度CallAdapter的adapt()方法在主线程调用因为 Retrofit 的create()在主线程执行但Call.enqueue()的回调默认在 OkHttp 的Dispatcher线程通常是后台线程。ResultCallAdapter的callbackFlow内部awaitClose也在后台线程。那么Flow.collect{}在主线程调用时如何保证emit()的数据在主线程消费答案是Flow的线程调度由collect侧决定而非emit侧。因此你必须在collect时指定flowOnlifecycleScope.launch { apiService.getUser(123) .flowOn(Dispatchers.IO) // 确保 emit 在 IO 线程 .catch { e - handleError(e) } .collect { result - // 这里在主线程执行 when (result) { is Result.Success - updateUI(result.data) is Result.Failure - showError(result.exception) } } }如果忘记flowOnemit()会在 OkHttp 线程执行而collect的 lambda 也会在该线程执行导致updateUI()报CalledFromWrongThreadException。这是新手最常踩的坑。记住CallAdapter只负责“发出数据”线程安全由Flow的构建和消费共同保障。5. 常见问题与避坑指南那些源码不会告诉你的细节5.1 问题速查表高频故障现象与根因分析现象可能根因排查步骤Flow不发射任何数据collect一直等待Call.enqueue()未被调用callbackFlow的awaitClose未触发OkHttp 超时设置过短1. 在adapt()中加日志Log.d(ResultCA, adapt called)2. 检查OkHttpClient的connectTimeout是否为 03. 确认callbackFlow的close()是否在onResponse/onFailure中被调用Result.failure()中的Throwable是NullPointerException但堆栈指向ConverterConverter的convert()方法返回null而CallAdapter未做空检查在adapt()的onResponse()中添加if (response.body() null) emit(Result.failure(EmptyBodyException()))同一个Flow被collect两次网络请求执行了两次Flow是冷流每次collect都会触发新请求未使用shareIn()或stateIn()在 Repository 层对Flow做共享apiService.getUser(id).shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replayCapacity 1)Call.cancel()后Flow仍收到Result.success()awaitClose未正确绑定call.cancel()callbackFlow的close()被提前调用检查awaitClose块内是否调用了call.cancel()确认onResponse/onFailure中close()是否在trySend后执行suspend fun接口返回User但ResultCallAdapter未被触发suspend fun的返回类型被 Kotlin 编译器转为ObjectgetRawType()无法识别Resultsuspend fun必须配合CoroutineCallAdapterFactory不能与ResultCallAdapter混用若需Result应显式声明fun(): FlowResultT5.2 实操心得三个被官方文档忽略的关键细节细节一CallAdapter的responseType()必须精确匹配Converter的输入类型responseType()返回的Type会直接传给Converter。如果你在Factory.get()中错误地写了getParameterUpperBound(1, ...)索引越界Converter会收到Object.class导致 Gson 解析成LinkedTreeMap。我曾在一个金融项目中遇到此问题后端返回{data: {name: A}}Converter解析出MapString, Any而 ViewModel 期望User结果Result.success()包含的是Map强转时报ClassCastException。解决方案在Factory.get()中添加类型校验日志val resultType getParameterUpperBound(0, returnType as ParameterizedType) Log.d(ResultCA, Resolved responseType: $resultType)并在Converter中打印type.toString()确保两者一致。细节二CallAdapter的adapt()方法不应持有Call的长期引用Call对象包含OkHttpClient引用如果adapt()返回的对象如Flow长期存活可能导致OkHttpClient无法被 GC引发内存泄漏。正确做法是在callbackFlow的awaitClose中确保call被释放。我们之前的实现已做到但要注意如果Flow被shareIn()共享awaitClose会在最后一个订阅者取消时才触发此时call可能已过期。因此对于长生命周期的Flow建议在Repository层做请求缓存而非依赖CallAdapter的长持。细节三CallAdapter无法拦截Converter的初始化异常Converter的convert()方法在CallAdapter的onResponse()之前执行但如果Converter的构造函数抛出异常如 Gson 配置错误这个异常会直接导致Retrofit.create()失败CallAdapter根本没有机会介入。因此Converter的健壮性测试必须独立于CallAdapter。我的做法是为每个Converter编写单元测试用Mockito模拟ResponseBody验证各种异常场景下的行为。5.3 性能陷阱为什么不要在 adapt() 中做耗时操作adapt()方法看似只是“返回一个 Flow”但如果你在里面做耗时操作比如override fun adapt(call: CallT): FlowResultT { val startTime System.currentTimeMillis() Thread.sleep(100) // 模拟耗时 Log.d(ResultCA, adapt took ${System.currentTimeMillis() - startTime}ms) return callbackFlow { /* ... */ } }这会导致Retrofit.create()调用变慢而create()通常在 Application 初始化或 Fragment 创建时调用会拖慢首屏渲染。adapt()应该是纯函数式的只做类型检查、对象创建、回调注册所有耗时操作如网络请求、磁盘读写必须延迟到Flow.collect或Callback触发时。这是CallAdapter设计的底层哲学——它是一个“蓝图”而非“执行器”。6. 进阶扩展从 ResultCallAdapter 到企业级网络层6.1 支持多状态封装StateFlowNetworkState ResultT只有 success/failure但真实 UI 需要 loading/idle/error。我们可以扩展CallAdapter返回StateFlowNetworkStateTsealed interface NetworkStateout T { data class LoadingT(val progress: Int 0) : NetworkStateT data class SuccessT(val data: T) : NetworkStateT data class ErrorT(val code: Int, val message: String) : NetworkStateT object IdleT : NetworkStateT }adapt()中onResponse()发送SuccessonFailure()发送Error并在enqueue()前发送Loading。关键是要在StateFlow的value更新时确保线程安全——StateFlow的value只能在协程中更新因此callbackFlow需要launchIn到指定CoroutineScope。这要求CallAdapter的构造函数接收CoroutineScope而Factory需要从Retrofit或外部注入。这种设计让网络状态与 UI 状态完全对齐避免了LiveData的postValue线程问题。6.2 与 DI 框架集成Hilt 中的 CallAdapter 注入在 Hilt 项目中CallAdapter的Factory可以作为Singleton注入Module InstallIn(SingletonComponent::class) object NetworkModule { Provides Singleton fun provideResultCallAdapterFactory(): CallAdapter.Factory ResultCallAdapterFactory() }然后在Retrofit的Builder中Provides Singleton fun provideRetrofit( okHttpClient: OkHttpClient, gsonConverterFactory: GsonConverterFactory, resultCallAdapterFactory: CallAdapter.Factory ): Retrofit Retrofit.Builder() .baseUrl(https://api.example.com/) .addConverterFactory(gsonConverterFactory) .addCallAdapterFactory(resultCallAdapterFactory) .build()这样ResultCallAdapter的实例由 Hilt 管理可注入Application上下文、SharedPreferences等依赖实现 Token 自动读取、请求日志上报等高级功能。6.3 测试驱动开发为 CallAdapter 编写 Robolectric 测试CallAdapter的逻辑可完全脱离 Android 环境测试。用 Robolectric 模拟CallTest fun adapt returns Success when response is successful() { // Given val mockCall mockCallUser() val mockResponse mockResponseUser() val user User(test) when(mockResponse.isSuccessful).thenReturn(true) when(mockResponse.body()).thenReturn(user) when(mockCall.execute()).thenReturn(mockResponse) val adapter ResultCallAdapterUser(Types.newParameterizedType(Result::class.java, User::class.java)) // When val result adapter.adapt(mockCall) // Then assertThat(result).isInstanceOf(Result.Success::class.java) }这种测试覆盖了adapt()的核心路径确保ResultCallAdapter的行为符合预期而不依赖真实网络。我在实际项目中把ResultCallAdapter作为网络层基座再叠加RetryCallAdapter自动重试、CacheCallAdapter内存/磁盘缓存最终形成一个声明式网络请求系统只需写GET(user) Retry(3) Cache(300) fun getUser(): FlowResultUser所有横切关注点自动注入。这套方案在三年内支撑了 12 个业务线零重大线上事故。它的核心不是代码多炫酷而是每个CallAdapter都严格遵循“单一职责”一个只管状态一个只管重试一个只管缓存。当你把CallAdapter看作网络层的“乐高积木”而不是“黑盒插件”Retrofit 的源码系列才真正从“阅读”变成了“驾驭”。
阅读完成 · 觉得有帮助?
咨询建站