如何使用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
相关产品推荐
相关产品推荐

