大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
网络层
这一课你将能做到
- 用 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 负责传输优化;统一错误处理和缓存策略是企业级网络层的核心。
延伸
- Retrofit 官方文档
- OkHttp 官方文档
- Apollo Android
- 下一课:异步与响应式——Coroutines、Flow 与 WorkManager。