网络与本地存储为什么要分层

几乎所有应用都要做两件事:从网络拿数据,把数据存到本地。如果直接在界面层写网络请求又直接写文件,代码很快就会变成一团——网络挂了怎么办、缓存怎么失效、数据格式变了怎么改,全都缠在一起。

正确的做法是分层:Retrofit 负责和网络对话,Room 负责本地落盘,中间用一个 Repository 统一决定"这次数据从哪来"。对界面而言,它只看见一个永远返回数据的接口,至于数据是来自网络还是缓存,界面并不关心。

把"数据从哪来"这个问题集中到 Repository,界面就永远不会因为网络策略调整而改动。这就是分层的回报。

Retrofit:声明式网络请求

Retrofit 用接口加注解的方式描述网络请求,把样板代码降到最低。你定义一个接口,方法上加 GET、POST、Path、Query 等注解,Retrofit 在编译期生成实现。

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Long): UserDto

    @GET("news")
    suspend fun fetchNews(
        @Query("page") page: Int,
        @Query("size") size: Int = 20
    ): List<NewsDto>
}

配合 Kotlin 协程,suspend 方法让网络请求像同步代码一样书写。Retrofit 会自动把它切到后台线程,你在 viewModelScope 里调用即可,不用自己管线程切换。

数据模型与错误处理

网络返回的 DTO 和界面用的实体不要混为一谈。DTO 跟服务端字段一一对应,实体是界面真正需要的样子,两者之间写一层映射,服务端改字段时只动映射,不污染业务。

错误处理三件套
  • HttpException非 2xx 的状态码,转成可读的业务错误。
  • IOException断网、超时,统一提示"网络不可用"。
  • 序列化异常字段对不上,别让它直接崩,兜底处理。
  • 统一封装在 Repository 里把异常转成 UiState.Error,界面只展示。
安卓网络与本地存储示意:左侧半透明云层经发光管道连接中央手机,手机下方连着方形本地数据库立方体,管道上有数据粒子
网络与本地:云端下来的数据先落 Room,界面优先读本地,断网也能看。

千万别在界面层 try/catch 一大堆网络细节,把这些都收敛进 Repository,界面只面对"成功的数据"和"一句错误提示"。

拦截器与公共参数

OkHttp 的拦截器是处理公共逻辑的好地方:加请求头、附 token、打印日志、统一处理过期重登。这些横切关注点不该写在每个接口里。

val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val req = chain.request().newBuilder()
            .header("Authorization", "Bearer $token")
            .header("Accept", "application/json")
            .build()
        chain.proceed(req)
    }
    .connectTimeout(10, TimeUnit.SECONDS)
    .build()

日志拦截器只在 debug 版本打开,release 包里关掉——线上打印接口内容既费性能又可能泄露用户数据。

Room:本地数据库 ORM

Room 是安卓官方的本地数据库 ORM,在 SQLite 之上提供编译期校验的查询接口。它由三部分组成:Entity(表)、Dao(数据访问)、Database(持有者)。

@Entity(tableName = "news")
data class NewsEntity(
    @PrimaryKey val id: Long,
    val title: String,
    val summary: String,
    val updatedAt: Long
)

@Dao
interface NewsDao {
    @Query("SELECT * FROM news ORDER BY updatedAt DESC")
    fun observeNews(): Flow<List<NewsEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(list: List<NewsEntity>)
}

DAO 方法返回 Flow 是 Room 的杀手锏:数据库一变,Flow 自动发射新值,界面随之刷新。这让"数据库即单一数据源"变得非常自然。

缓存优先与离线可用

缓存优先的思路是:界面先订阅 Room 的 Flow,数据立刻有显示;后台再从网络拉最新数据,写回 Room;Room 一变,界面自动刷新。这样即使断网,上次的数据也还在。

  • 单一数据源:界面只订阅 Room,不直接碰网络。
  • 网络成功才写库:网络失败时保留旧缓存,不清空。
  • 过期策略:给缓存加时间戳,超过阈值才强制刷新。
  • 分页数据:本地表按页或按列表维护,避免越攒越大。
离线可用不是加个缓存就完事,而是把 Room 当成界面唯一的数据来源,网络只负责回填。这个顺序颠倒,体验就差一截。