CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
网络层 Retrofit/OkHttp/Apollo。
网络层
这一课你将能做到
用 Retrofit 定义类型安全的 REST API 接口,处理 GET/POST/PUT/DELETE
配置 OkHttp 拦截器实现日志、鉴权、缓存、重试
对比 REST 与 GraphQL 在移动端的适用场景,了解 Apollo Android 基础
为什么要学
网络是 App 的「输血管」。没有网络层,App 就是孤岛;网络层设计不好,用户体验就是灾难。Retrofit 是 Android 网络的事实标准,OkHttp 是底层引擎。理解它们,才能处理鉴权、缓存、错误、重试这些真实世界的复杂场景。
人话:Retrofit 就像「API 的翻译官」。你写 Kotlin 接口,它帮你翻译成 HTTP 请求;服务器返回 JSON,它帮你翻译成 Kotlin 对象。OkHttp 是「快递员」,负责真正发送和接收包裹。
面试中,「Retrofit 原理」、「OkHttp 拦截器链」、「REST vs GraphQL」是高频考点。
先建立图像
Retrofit + OkHttp 工作流程:
┌─────────────┐ 接口定义 ┌─────────────┐
│ TrackApi │ ───────────────→ │ Retrofit │
│ interface │ @GET("tracks") │ (动态代理) │
└─────────────┘ └──────┬──────┘
│
┌─────────┴─────────┐
│ OkHttp 拦截器链 │
│ ┌─────────────┐ │
│ │ AuthInterceptor │ │
│ ├─────────────┤ │
│ │ CacheInterceptor│ │
│ ├─────────────┤ │
│ │ LoggingInterceptor│
│ └─────────────┘ │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ HTTP 请求/响应 │
│ api.coderoadmap │
└───────────────────┘
跟着做
步骤 1:Retrofit 接口定义
// DTO(与服务器 JSON 对应)
data class TrackDto(
val slug: String,
val title: String,
val description: String,
@SerializedName("lesson_count") val lessonCount: Int,
@SerializedName("cover_url") val coverUrl: String?,
@SerializedName("updated_at") val updatedAt: String
)
data class TrackDetailDto(
val slug: String,
val title: String,
val lessons: List<LessonDto>
)
data class LessonDto(
val slug: String,
val title: String,
val content: String,
val duration: Int
)
// API 接口
interface TrackApiService {
@GET("tracks")
suspend fun getTracks(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20
): ApiResponse<List<TrackDto>>
@GET("tracks/{slug}")
suspend fun getTrackDetail(@Path("slug") slug: String): ApiResponse<TrackDetailDto>
@POST("tracks/{slug}/favorite")
suspend fun toggleFavorite(
@Path("slug") slug: String,
@Body request: FavoriteRequest
): ApiResponse<Unit>
@PUT("user/progress")
suspend fun updateProgress(@Body progress: ProgressUpdate): ApiResponse<Unit>
@DELETE("tracks/{slug}")
suspend fun deleteTrack(@Path("slug") slug: String): ApiResponse<Unit>
}
// 统一响应包装
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T
)
data class FavoriteRequest(val favorite: Boolean)
data class ProgressUpdate(val trackSlug: String, val lessonSlug: String, val progress: Int)
步骤 2:Retrofit 与 OkHttp 配置 @Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideGson(): Gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.create()
@Provides
@Singleton
fun provideAuthInterceptor(
tokenManager: TokenManager
): Interceptor = Interceptor { chain ->
val token = tokenManager.getToken()
val request = chain.request().newBuilder()
.apply {
if (token != null) {
addHeader("Authorization", "Bearer $token")
}
addHeader("Accept", "application/json")
addHeader("Content-Type", "application/json")
}
.build()
chain.proceed(request)
}
@Provides
@Singleton
fun provideCacheInterceptor(): Interceptor = Interceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
// GET 请求缓存 5 分钟
if (request.method == "GET") {
response.newBuilder()
.header("Cache-Control", "public, max-age=300")
.build()
} else {
response
}
}
@Provides
@Singleton
fun provideOkHttpClient(
authInterceptor: Interceptor,
cacheInterceptor: Interceptor,
@ApplicationContext context: Context
): OkHttpClient {
val cacheSize = 10L * 1024 * 1024 // 10MB
val cache = Cache(context.cacheDir, cacheSize)
return OkHttpClient.Builder()
.cache(cache)
.addInterceptor(authInterceptor)
.addInterceptor(cacheInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
}
@Provides
@Singleton
fun provideRetrofit(
client: OkHttpClient,
gson: Gson
): Retrofit = Retrofit.Builder()
.baseUrl("https://api.coderoadmap.com/v1/")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
@Provides
@Singleton
fun provideTrackApi(retrofit: Retrofit): TrackApiService =
retrofit.create(TrackApiService::class.java)
}
步骤 3:统一错误处理 sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val code: Int, val message: String) : NetworkResult<Nothing>()
data class Exception(val throwable: Throwable) : NetworkResult<Nothing>()
}
suspend fun <T> safeApiCall(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
apiCall: suspend () -> ApiResponse<T>
): NetworkResult<T> = withContext(dispatcher) {
try {
val response = apiCall()
if (response.code == 0) {
NetworkResult.Success(response.data)
} else {
NetworkResult.Error(response.code, response.message)
}
} catch (e: IOException) {
NetworkResult.Exception(e)
} catch (e: HttpException) {
NetworkResult.Error(e.code(), e.message())
} catch (e: Exception) {
NetworkResult.Exception(e)
}
}
// 在 Repository 中使用
suspend fun getTracks(): NetworkResult<List<TrackDto>> = safeApiCall {
api.getTracks()
}
步骤 4:ViewModel 消费网络数据 @HiltViewModel
class TrackListViewModel @Inject constructor(
private val repository: TrackRepository
) : ViewModel() {
data class UiState(
val tracks: List<TrackUiModel> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val isEmpty: Boolean = false
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
loadTracks()
}
fun loadTracks() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
when (val result = repository.getTracks()) {
is NetworkResult.Success -> {
_uiState.update { state ->
state.copy(
tracks = result.data.map { it.toUiModel() },
isLoading = false,
isEmpty = result.data.isEmpty()
)
}
}
is NetworkResult.Error -> {
_uiState.update {
it.copy(
isLoading = false,
error = "服务器错误: ${result.message}"
)
}
}
is NetworkResult.Exception -> {
_uiState.update {
it.copy(
isLoading = false,
error = "网络异常,请检查连接"
)
}
}
}
}
}
}
步骤 5:Apollo GraphQL 基础(了解) GraphQL 在移动端的优势:按需取数、减少请求次数、强类型 Schema。
// schema.graphqls 定义
type Track {
slug: String!
title: String!
description: String
lessons: [Lesson!]!
}
type Query {
tracks(limit: Int): [Track!]!
track(slug: String!): Track
}
// Apollo 查询
query GetTracks($limit: Int) {
tracks(limit: $limit) {
slug
title
description
lessons {
slug
title
}
}
}
// Kotlin 使用
val response = apolloClient.query(GetTracksQuery(limit = 20)).execute()
val tracks = response.data?.tracks
Apollo 生成类型安全的 Kotlin 模型,与 Retrofit 类似但基于 Schema。
讲清楚要点
Retrofit 是「动态代理 + 注解解析」 :Retrofit.create() 在运行时生成接口实现,把注解翻译成 HTTP 请求。不是反射调用,是编译期生成辅助类 + 运行时代理。
OkHttp 拦截器分两种 :Application Interceptor(最外层,能看到最终请求/响应)和 Network Interceptor(能看到重定向、中间响应)。鉴权用前者,缓存用后者。
suspend 函数是 Retrofit 的「一等公民」 :Retrofit 2.6+ 原生支持 suspend,自动处理线程切换,无需 Call.enqueue 回调地狱。
错误处理要分层 :网络层返回 NetworkResult(成功/业务错误/异常),ViewModel 转换为 UI 状态,Compose 显示对应界面。每层职责清晰。
GraphQL 不是银弹 :按需取数减少流量,但 Schema 设计复杂,缓存比 REST 难。移动端弱网环境 REST + 本地缓存可能更可靠。
你可能会懵的地方
Retrofit 和 OkHttp 到底谁做什么?
Retrofit 是「接口翻译器」,负责把 Kotlin 接口变成 HTTP 请求。OkHttp 是「HTTP 客户端」,负责真正发送请求、管理连接池、处理响应。Retrofit 底层默认用 OkHttp。
@SerializedName 一定要写吗?
如果 Kotlin 字段名与 JSON 完全一致,可以不写。但建议写:1. 防止混淆;2. 服务器改字段名时只改注解;3. 支持特殊字符。
拦截器里能做异步操作吗?
不能。拦截器是同步阻塞的。需要异步(如刷新 Token)时,用 Authenticator 或在外层处理。
如何取消请求?
Retrofit 的 suspend 函数自动随协程取消。viewModelScope.launch 在 ViewModel 销毁时自动取消,防止内存泄漏。
WebSocket 用 Retrofit 吗?
不用。Retrofit 是 HTTP 请求-响应模型。WebSocket 用 OkHttp 的 newWebSocket 或专用库如 Scarlet。
错法对照 错法 问题 正解 在 Activity 里直接写网络请求 无法测试,生命周期泄漏 Repository 封装,ViewModel 调度 忽略 HTTP 状态码 404/500 显示成功 检查 response.isSuccessful 或统一封装 在拦截器里做复杂逻辑 阻塞请求,性能差 简单处理,复杂逻辑外移 硬编码 Token 在代码里 安全风险,更新困难 TokenManager 统一管理,过期自动刷新 不处理网络超时 用户无限等待 设置 connect/read/write timeout
卡点急救
请求成功但数据为 null :检查 JSON 字段名是否与 DTO 匹配,特别是 @SerializedName。用 Logcat 查看原始响应。
拦截器不生效 :检查是否 addInterceptor 到正确的 OkHttpClient 实例。多个 OkHttpClient 实例可能导致配置分散。
POST 请求参数格式错误 :检查 @Body 对象是否有无参构造函数(Gson 需要),或改用 @FormUrlEncoded + @Field。
面试常问
Retrofit 的工作原理?
动态代理:Retrofit.create() 生成接口代理;注解解析:把 @GET、@Path 等解析成 Request;Converter:把 Java 对象转 JSON(Gson);CallAdapter:把 Call 转成 suspend 或 Observable。
OkHttp 的拦截器链执行顺序?
Application Interceptor → RetryAndFollowUp → Bridge → Cache → Connect → Network Interceptor → CallServer。响应时反向。Application 层能看到最终请求,Network 层能看到网络细节。
REST 和 GraphQL 在移动端的取舍?
REST:成熟、缓存简单、适合简单 CRUD;GraphQL:按需取数、减少请求、适合复杂关联数据。弱网环境 REST + 本地缓存更可靠;网络好、数据复杂时 GraphQL 有优势。
自测三问
解释 Retrofit 中 ConverterFactory 和 CallAdapterFactory 的作用。
设计一个拦截器链:日志 → 鉴权 → 缓存,说明每层为什么必须在这个位置。
什么场景下应该选择 GraphQL 而不是 REST?举一个具体业务例子。
小结一句话 Retrofit + OkHttp 是 Android 网络的黄金搭档:Retrofit 负责接口抽象,OkHttp 负责传输优化;统一错误处理和缓存策略是企业级网络层的核心。
延伸