ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

1. 项目概述:为什么自定义 CallAdapter 是 Retrofit 真正的“开关”

你写过 Retrofit 的接口,也用过Call<T>和LiveData<T>,甚至可能在项目里混着用RxJava2CallAdapterFactory和CoroutineCallAdapterFactory。但有没有哪一刻,你盯着@GET("user") suspend fun getUser(): User这行代码发愣——它到底怎么从网络响应变成一个挂起函数的?又或者,当你想把某个接口的返回统一包装成Result<T>(比如Result<User>),却不得不在每个onResponse()里手动套一层,手酸到想砸键盘?这些不是使用问题,而是 Retrofit 的“能力边界”没被你真正打开。CallAdapter 就是那个边界开关——它不处理网络请求,也不解析 JSON,但它决定了“请求完成之后,结果该以什么形态交到你手上”。Retrofit 默认只认Call<T>,但你的业务需要Flow<T>、Deferred<T>、StateFlow<ApiResponse<T>>,甚至是一个带重试逻辑的SmartCall<T>,这时候,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): Call<User> }

这里Call<User>是 Retrofit 唯一原生支持的返回类型。但当你换成:

@GET("user/{id}") suspend fun getUser(@Path("id") id: Int): User

Retrofit 并没有报错,反而能正常工作。为什么?因为suspend函数在编译后会被 Kotlin 编译器转成Continuation<User>,而 Retrofit 的RequestFactory在解析方法签名时,会检查返回类型是否实现了CallAdapter的适配规则。关键点在于:Retrofit 本身并不关心你返回的是Call、Flow还是suspend,它只关心“有没有一个 CallAdapter 能接住这个类型,并承诺把它变成 Call 或直接执行网络请求”。这就是CallAdapter的本质——它不是数据格式转换器(那是Converter的事),而是一份交付协议:你告诉我你要什么形态的结果(T),我负责按约定把网络响应、异常、取消信号打包成你指定的容器,并确保生命周期、线程、错误传播都符合你的预期。

2.2 CallAdapter 的三层职责模型

我把CallAdapter的实际工作拆成三个不可分割的层次,这是理解所有自定义行为的基础:

第一层:类型声明与契约绑定
CallAdapter的responseType()方法必须返回User(即T),而不是Flow<User>或Deferred<User>。这意味着CallAdapter的核心任务之一,是“剥掉外层容器,暴露真实业务数据类型”。比如FlowCallAdapter的responseType()返回User,而它自己内部管理Flow<User>的创建和发射。这个设计强制你明确区分“数据载体”和“数据本体”,避免像早期某些封装库那样,把LiveData<User>当作T,导致泛型擦除后无法正确解析 JSON。

第二层:执行时机与生命周期托管
CallAdapter的adapt()方法接收一个原始的Call<T>,但返回的却可能是Flow<T>或Deferred<T>。这里的关键是:adapt()不执行网络请求,它只是“注册监听”或“准备执行”。真正的请求触发发生在你调用flow.collect{}或deferred.await()的那一刻。Retrofit 把执行权完全交给你——Flow的collect启动请求,Deferred的await启动请求,suspend fun的调用本身启动请求。这种设计让CallAdapter能天然支持懒加载、多次收集、取消传播等高级特性,但也意味着你必须在adapt()中正确处理Call.cancel()与Flow.cancellation的映射,否则会出现“界面退出后请求还在后台跑”的经典问题。

第三层:错误与状态的语义化封装
默认Call<T>只提供onResponse()和onFailure(),错误信息藏在Throwable里。但业务上你需要区分“网络超时”“服务器返回 500”“JSON 解析失败”“Token 过期需跳登录”。CallAdapter允许你在adapt()中注入统一的状态处理器。比如ResultCallAdapter会把所有异常包装成Result.failure(throwable),而成功响应包装成Result.success(data),这样 ViewModel 层拿到的永远是Result<User>,无需每个地方if (it.isSuccessful) ... else ...。但注意:这种封装不能破坏 Retrofit 的异常传播链。如果Converter解析失败抛出JsonParseException,CallAdapter必须确保这个异常能被上层捕获,而不是静默吞掉——否则你会陷入“接口明明失败了,但 Flow 就是不发射任何东西”的调试地狱。

2.3 为什么不能只靠 Converter 解决所有问题?

常有开发者问:“我能不能在Converter里直接返回Result<T>,然后Call<Result<T>>就完事了?” 答案是:技术上可行,但逻辑上灾难。Converter的职责是数据格式转换,它只管ResponseBody→T这一段。而Result<T>的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 的实操实现:从零手写一个 ResultCallAdapter

3.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无关,但你的Result<T>需要能被 Gson 正确解析,所以GsonConverterFactory仍是必需的。另外,不要在build.gradle中添加retrofit2-kotlin-coroutines-adapter,我们要从零实现,才能看清每一步的取舍。

3.2 核心类定义与 Factory 模式

首先定义ResultCallAdapter类,它必须继承CallAdapter<Result<T>, Call<T>>(注意泛型顺序:CallAdapter<R, T>中R是返回类型,T是响应类型):

class ResultCallAdapter<T>(private val responseType: Type) : CallAdapter<Result<T>, Call<T>> { override fun responseType(): Type = responseType override fun adapt(call: Call<T>): Result<T> { // 这里是核心逻辑,稍后展开 TODO("Not yet implemented") } }

接着实现Factory,这是 Retrofit 发现并加载CallAdapter的入口:

class ResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, 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 ResultCallAdapter<Any>(resultType) as CallAdapter<*, *> } }

这里有两个关键技巧:

  • getRawType(returnType)用于获取Result<User>的原始类Result,避免因泛型擦除无法匹配;
  • getParameterUpperBound(0, ...)用于提取Result<User>中的User类型,这是后续Converter解析 JSON 的依据。Retrofit 内部大量使用Utils工具类处理泛型,我们复用它保证兼容性。

3.3 adapt() 方法的完整实现:线程、异常、取消的三重编织

现在进入最核心的adapt()方法。注意:这里不能直接执行网络请求,也不能阻塞主线程。我们的目标是返回一个Result<T>,但Result是密封类,无法直接实例化。因此,我们必须返回一个“可计算出 Result 的对象”,而Result本身是不可变的,所以实际返回的是Result<T>的生产者——一个suspend fun或Callable<Result<T>>。但 Retrofit 要求adapt()返回Result<T>,这就矛盾了。解决方案是:返回一个惰性计算的 Result,其内部封装了 Call 的执行逻辑。标准做法是返回一个Result<T>的包装类,但我们选择更简洁的方式——利用 Kotlin 的runCatching:

override fun adapt(call: Call<T>): Result<T> { 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设计允许你返回任何类型,只要它能“代表”最终结果。因此,更合理的方案是返回一个Deferred<Result<T>>或Flow<Result<T>>。我们选择Flow,因为它天然支持取消、多次收集、生命周期绑定:

class ResultCallAdapter<T>(private val responseType: Type) : CallAdapter<Flow<Result<T>>, Call<T>> { override fun responseType(): Type = responseType override fun adapt(call: Call<T>): Flow<Result<T>> { 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: Call<T>): Flow<Result<T>> { return callbackFlow { val callback = object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { trySend( if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(HttpException(response)) } ) close() // Flow 收集完成,关闭通道 } override fun onFailure(call: Call<T>, 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: Array<Annotation>, 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): Result<User>

而其他接口保持默认行为。这种基于注解的动态适配,是CallAdapter灵活性的集中体现,也是它比简单封装工具类更强大的原因——它把策略决策权交还给接口定义本身,而非全局配置。

4. 深度实操:与 Converter、Interceptor 的协同作战

4.1 CallAdapter 与 Converter 的调用时序图解

很多开发者混淆CallAdapter和Converter的执行顺序。用一个真实请求链来说明:

  1. 你调用apiService.getUser(123).collect{};
  2. ResultCallAdapter.adapt()被触发,内部调用call.enqueue(callback);
  3. OkHttp 发起网络请求,收到ResponseBody;
  4. Retrofit 调用Converter.convert(responseBody, responseType),将字节流转为User对象;
  5. Converter返回User,Callback.onResponse()被调用;
  6. ResultCallAdapter的onResponse()回调中,emit(Result.success(user))。

关键点:Converter在CallAdapter的onResponse()之前执行,且只对成功响应生效。如果Converter解析失败(如 JSON 字段缺失),它会抛出RuntimeException,这个异常会直接传递给Callback.onFailure(),进而被ResultCallAdapter包装成Result.failure(exception)。因此,Converter的健壮性直接影响Result<T>的failure分支内容。建议在自定义Converter中对常见解析异常做分类:

class SafeGsonConverter<T>(private val gson: Gson, private val type: Type) : Converter<ResponseBody, 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: gzip),Converter必须能正确解压,否则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时指定flowOn:

lifecycleScope.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) } } }

如果忘记flowOn,emit()会在 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是否为 0;3. 确认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 编译器转为Object,getRawType()无法识别Resultsuspend fun必须配合CoroutineCallAdapterFactory,不能与ResultCallAdapter混用;若需Result,应显式声明fun(): Flow<Result<T>>

5.2 实操心得:三个被官方文档忽略的关键细节

细节一:CallAdapter的responseType()必须精确匹配Converter的输入类型
responseType()返回的Type,会直接传给Converter。如果你在Factory.get()中错误地写了getParameterUpperBound(1, ...)(索引越界),Converter会收到Object.class,导致 Gson 解析成LinkedTreeMap。我曾在一个金融项目中遇到此问题:后端返回{"data": {"name": "A"}},Converter解析出Map<String, 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: Call<T>): Flow<Result<T>> { 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 支持多状态封装:StateFlow<NetworkState >

Result<T>只有 success/failure,但真实 UI 需要 loading/idle/error。我们可以扩展CallAdapter,返回StateFlow<NetworkState<T>>:

sealed interface NetworkState<out T> { data class Loading<T>(val progress: Int = 0) : NetworkState<T> data class Success<T>(val data: T) : NetworkState<T> data class Error<T>(val code: Int, val message: String) : NetworkState<T> object Idle<T> : NetworkState<T> }

adapt()中,onResponse()发送Success,onFailure()发送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 模拟Call:

@Test fun `adapt returns Success when response is successful`() { // Given val mockCall = mock<Call<User>>() val mockResponse = mock<Response<User>>() val user = User("test") `when`(mockResponse.isSuccessful).thenReturn(true) `when`(mockResponse.body()).thenReturn(user) `when`(mockCall.execute()).thenReturn(mockResponse) val adapter = ResultCallAdapter<User>(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(): Flow<Result<User>>,所有横切关注点自动注入。这套方案在三年内支撑了 12 个业务线,零重大线上事故。它的核心不是代码多炫酷,而是每个CallAdapter都严格遵循“单一职责”:一个只管状态,一个只管重试,一个只管缓存。当你把CallAdapter看作网络层的“乐高积木”,而不是“黑盒插件”,Retrofit 的源码系列,才真正从“阅读”变成了“驾驭”。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进