You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Ktor Client缓存API的JSON响应并实现离线可用与更新?

实现Ktor Client的JSON响应离线缓存方案

Ktor自带的HttpCache仅遵循HTTP缓存头规则,无法满足自定义离线缓存需求,我们可以通过自定义拦截器+本地存储的方式实现需求,核心逻辑是根据网络状态决定读取缓存还是发起请求并更新缓存。

步骤1:实现网络状态检测工具

先实现一个工具类判断设备当前网络连接状态:

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities

fun Context.isNetworkAvailable(): Boolean {
    val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val network = connectivityManager.activeNetwork ?: return false
    val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}

步骤2:实现本地缓存存储(以Room为例)

用Room存储结构化的缓存数据,适配JSON响应的存储需求:

定义缓存实体

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "api_cache")
data class ApiCache(
    @PrimaryKey val cacheKey: String, // 请求唯一标识
    val responseJson: String, // 缓存的JSON响应内容
    val timestamp: Long // 缓存时间戳,用于后续过期判断
)

定义DAO接口

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface ApiCacheDao {
    @Query("SELECT * FROM api_cache WHERE cacheKey = :key")
    suspend fun getCacheByKey(key: String): ApiCache?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrUpdateCache(cache: ApiCache)
}

创建Room数据库

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [ApiCache::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun apiCacheDao(): ApiCacheDao
}

步骤3:自定义Ktor拦截器实现缓存逻辑

通过Ktor的HttpSend拦截器,结合网络状态和本地缓存处理请求:

import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpMethod
import io.ktor.util.AttributeKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

// 定义属性键,用于标记是否跳过缓存(比如强制刷新场景)
val SkipCacheKey = AttributeKey<Boolean>("SkipCache")

fun HttpClient.configureOfflineCache(context: Context, db: AppDatabase) {
    install(HttpSend) {
        intercept { request ->
            val skipCache = request.attributes.getOrNull(SkipCacheKey) ?: false
            val cacheKey = generateCacheKey(request)

            // 无网络且不跳过缓存时,读取本地缓存返回
            if (!context.isNetworkAvailable() && !skipCache) {
                val cache = withContext(Dispatchers.IO) {
                    db.apiCacheDao().getCacheByKey(cacheKey)
                }
                cache?.let {
                    return@intercept HttpResponse(request).apply {
                        body = it.responseJson
                    }
                }
            }

            // 有网络或需要跳过缓存时,发起实际请求
            val response = execute(request)

            // 请求成功时更新本地缓存
            if (response.status.value in 200..299 && !skipCache) {
                val responseJson = response.bodyAsText()
                withContext(Dispatchers.IO) {
                    db.apiCacheDao().insertOrUpdateCache(
                        ApiCache(
                            cacheKey = cacheKey,
                            responseJson = responseJson,
                            timestamp = System.currentTimeMillis()
                        )
                    )
                }
            }

            response
        }
    }
}

// 根据请求的方法、URL、请求体生成唯一缓存键,确保缓存对应正确请求
private fun generateCacheKey(request: HttpRequestBuilder): String {
    val url = request.url.buildString()
    val method = request.method.value
    val bodyHash = request.body.toString().hashCode().toString()
    return "$method:$url:$bodyHash"
}

步骤4:使用配置好的HttpClient

// 初始化Room数据库
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()

// 配置HttpClient
val client = HttpClient {
    // 其他基础配置(如超时、Json序列化)
    install(HttpTimeout) {
        requestTimeoutMillis = 10000
    }
    // 应用自定义缓存逻辑
    configureOfflineCache(context, db)
}

// 发起请求示例
suspend fun fetchData(): String {
    return client.get {
        url("https://api.example.com/data")
        // 如需强制刷新,可开启跳过缓存
        // attributes.put(SkipCacheKey, true)
    }.bodyAsText()
}

关键补充说明

  • 缓存键优化:如果请求包含复杂参数,可对参数进行标准化排序后再生成缓存键,避免参数顺序不同导致缓存不命中。
  • 缓存过期策略:可在读取缓存时添加时间判断,比如缓存超过24小时则主动发起请求更新,避免展示过期数据。
  • 异常 fallback:可在请求失败(有网络但请求超时/报错)时,尝试读取缓存作为兜底返回,提升用户体验。

内容的提问来源于stack exchange,提问作者Osman Gani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 12:25:17