CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Android 开发入门/课时/网络层
RawBlame
课时目录12 节

Android 开发入门

  1. 01Android 开发概览
  2. 02四大组件
  3. 03传统 View UI
  4. 04Jetpack Compose
  5. 05架构模式
  6. 06依赖注入
  7. 07本地存储
  8. 08网络层
  9. 09异步与响应式
  10. 10Firebase 生态
  11. 11测试与质量
  12. 12综合练习

网络层

Retrofit/OkHttp/Apollo。

30 分钟

网络层

这一课你将能做到

  1. 用 Retrofit 定义类型安全的 REST API 接口,处理 GET/POST/PUT/DELETE
  2. 配置 OkHttp 拦截器实现日志、鉴权、缓存、重试
  3. 对比 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。

讲清楚要点

  1. Retrofit 是「动态代理 + 注解解析」:Retrofit.create() 在运行时生成接口实现,把注解翻译成 HTTP 请求。不是反射调用,是编译期生成辅助类 + 运行时代理。

  2. OkHttp 拦截器分两种:Application Interceptor(最外层,能看到最终请求/响应)和 Network Interceptor(能看到重定向、中间响应)。鉴权用前者,缓存用后者。

  3. suspend 函数是 Retrofit 的「一等公民」:Retrofit 2.6+ 原生支持 suspend,自动处理线程切换,无需 Call.enqueue 回调地狱。

  4. 错误处理要分层:网络层返回 NetworkResult(成功/业务错误/异常),ViewModel 转换为 UI 状态,Compose 显示对应界面。每层职责清晰。

  5. 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

卡点急救

  1. 请求成功但数据为 null:检查 JSON 字段名是否与 DTO 匹配,特别是 @SerializedName。用 Logcat 查看原始响应。

  2. 拦截器不生效:检查是否 addInterceptor 到正确的 OkHttpClient 实例。多个 OkHttpClient 实例可能导致配置分散。

  3. 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 有优势。

自测三问

  1. 解释 Retrofit 中 ConverterFactory 和 CallAdapterFactory 的作用。
  2. 设计一个拦截器链:日志 → 鉴权 → 缓存,说明每层为什么必须在这个位置。
  3. 什么场景下应该选择 GraphQL 而不是 REST?举一个具体业务例子。

小结一句话

Retrofit + OkHttp 是 Android 网络的黄金搭档:Retrofit 负责接口抽象,OkHttp 负责传输优化;统一错误处理和缓存策略是企业级网络层的核心。

延伸

  • Retrofit 官方文档
  • OkHttp 官方文档
  • Apollo Android
  • 下一课:异步与响应式——Coroutines、Flow 与 WorkManager。
加载本节练习…
← 上一课本地存储下一课 →异步与响应式