需求
- 能序列化并缓存数据
- 能获取缓存的数据并反序列化
- 能判断缓存是否过期
- 不缓存无效数据(为 empty 的 Map、Collection、Array 对象,以及为 null 的对象)
添加依赖
// 腾讯 MMKV:https://github.com/Tencent/MMKV
implementation 'com.tencent:mmkv-static:1.2.15'
// Json 解析框架:https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.10.1'
// 可选
// 工具类:https://github.com/Blankj/AndroidUtilCode
implementation 'com.blankj:utilcodex:1.31.1'
// 可选
// 日志打印框架:https://github.com/JakeWharton/timber
implementation 'com.jakewharton.timber:timber:5.0.1'
实现步骤
定义一个缓存接口
DataCache.kt
import java.lang.reflect.Type
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2023/06/14
* desc : 数据缓存接口。若要实现缓存逻辑,请实现该接口。
*/
interface DataCache {
/**
* 检查缓存是否过期
*/
fun checkExpired(cacheKey: String, expireDuration: Long): Boolean
/**
* 获取缓存时间的 Key
*/
fun getCacheTimeKey(cacheKey: String) = DATA_CACHE_TIME_KEY + cacheKey
/**
* 获取缓存数据的 Key
*/
fun getCacheDataKey(cacheKey: String) = DATA_CACHE_DATA_KEY + cacheKey
/**
* 缓存数据
*/
fun onCache(cacheKey: String, data: Any?)
/**
* 从缓存获取数据
*/
fun <T> getFromCache(cacheKey: String, type: Type): T?
companion object {
private const val DATA_CACHE_TIME_KEY = "DATA_CACHE_TIME_KEY_"
private const val DATA_CACHE_DATA_KEY = "DATA_CACHE_DATA_KEY_"
}
}
接口的实现
我们采用 MMKV 进行缓存的实现,使用 Gson 进行数据的序列化和反序列化。其中我们还使用到了泛型。
import com.blankj.utilcode.util.GsonUtils
import com.tencent.mmkv.MMKV
import java.lang.reflect.Type
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2023/06/14
* desc : 默认的数据缓存实现
*/
class DefaultCacheImpl : DataCache {
override fun checkExpired(cacheKey: String, expireDuration: Long): Boolean {
val cacheTime = mMMKV.decodeLong(getCacheTimeKey(cacheKey), 0L)
return System.currentTimeMillis() - cacheTime > expireDuration
}
override fun onCache(cacheKey: String, data: Any?) {
val jsonData = mGson.toJson(data)
mMMKV.encode(getCacheTimeKey(cacheKey), System.currentTimeMillis())
mMMKV.encode(getCacheDataKey(cacheKey), jsonData)
}
override fun <T> getFromCache(cacheKey: String, type: Type): T? {
val jsonData = mMMKV.decodeString(getCacheDataKey(cacheKey), null)
return mGson.fromJson(jsonData, type) as T?
}
companion object {
private const val MMKV_CACHE_KEY = "MMKV_CACHE_DATA"
private val mGson = GsonUtils.getGson()
private val mMMKV by lazy { MMKV.mmkvWithID(MMKV_CACHE_KEY, MMKV.MULTI_PROCESS_MODE) }
}
}
编写工具类
import cn.cqautotest.sunnybeach.util.cache.DataCache
import cn.cqautotest.sunnybeach.util.cache.DefaultCacheImpl
import com.google.gson.reflect.TypeToken
import timber.log.Timber
import java.lang.reflect.Type
import java.util.concurrent.TimeUnit
/**
* author : A Lonely Cat
* github : https://github.com/anjiemo/SunnyBeach
* time : 2022/05/26
* desc : 数据缓存帮助类,可以用于缓存网络请求的数据(一般只缓存 GET 请求返回的数据)。
* 不建议缓存时效性较要求高的数据。
* 不支持缓存接口对象 或 元素为接口类型的数组和集合,否则会在反序列化的时候失败,从而获取到无效数据,此问题由 Gson 产生。如:
* interface Data
* Collection<Data>
* Array<Data>
*/
object CacheHelper {
private val mCacheImpl: DataCache = DefaultCacheImpl()
/**
* 默认为 15 分钟失效,失效后需要重新获取数据并进行缓存
*/
fun checkExpired(cacheKey: String, expireDuration: Long = TimeUnit.MINUTES.toMillis(15)): Boolean =
mCacheImpl.checkExpired(cacheKey, expireDuration)
inline fun <reified T> getFromCache(cacheKey: String): T? = getFromCache(cacheKey, object : TypeToken<T>() {}.type)
/**
* 从本地缓存获取数据
*/
fun <T> getFromCache(cacheKey: String, type: Type): T? {
return try {
mCacheImpl.getFromCache(cacheKey, type)
} catch (t: Throwable) {
Timber.e(t, "are you ok? get cache failed.")
null
}
}
/**
* 保存数据到本地缓存
*/
fun saveToCache(cacheKey: String, data: Any?) {
if (!isValidateData(data)) return
try {
mCacheImpl.onCache(cacheKey, data)
} catch (t: Throwable) {
Timber.e(t)
}
}
/**
* 校验数据
*/
private fun isValidateData(data: Any?) = when (data) {
null -> false
is Map<*, *> -> data.isNotEmpty()
is Collection<*> -> data.isNotEmpty()
is Array<*> -> data.isNotEmpty()
else -> true
}
}
使用方式
缓存普通实体类对象数据
suspend fun loadUserInfo(): UserInfo {
// 检查缓存的数据是否过期
takeUnless { CacheHelper.checkExpired(cacheKey, TimeUnit.HOURS.toMillis(1)) }?.let {
// 数据未过期,从缓存中获取数据,如果反序列成功则返回该数据
CacheHelper.getFromCache<UserInfo>(cacheKey)?.let { return it }
}
// 未缓存 or 反序列化失败,重新从网络获取数据
val userInfo = loadUserInfoFromNetwork()
// 保存该数据到缓存中并返回该数据
return userInfo.also { CacheHelper.saveToCache(cacheKey, it) }
}
private suspend fun loadUserInfoFromNetwork(): UserInfo {
TODO("从网络获取数据并返回")
}
缓存集合对象数据
suspend fun listUserInfo(): List<UserInfo> {
// 检查缓存的数据是否过期
takeUnless { CacheHelper.checkExpired(cacheKey, TimeUnit.HOURS.toMillis(1)) }?.let {
// 数据未过期,从缓存中获取数据,如果反序列成功则返回该数据
val dataFromCache = CacheHelper.getFromCache<List<UserInfo>>(cacheKey).orEmpty()
// 如果数据不为 empty 则证明反序列化成功了,返回该数据
if (dataFromCache.isNotEmpty()) {
return dataFromCache
}
}
// 未缓存 or 反序列化失败,重新从网络获取数据
val userInfoList = listUserInfoFromNetwork()
// 保存该数据到缓存中并返回该数据
return userInfoList.also { CacheHelper.saveToCache(cacheKey, it) }
}
private suspend fun listUserInfoFromNetwork(): List<UserInfo> {
TODO("从网络获取数据并返回")
}
特别注意的点
- 推荐只缓存实体类对象
- 不要缓存接口对象,否则反序列时 Gson 无法实例化接口对象,从而导致数据为 null
- 不要缓存集合或数组中的元素为接口对象的对象,原因同上
通过这篇文章你能学到什么
- Json 解析框架 Gson的使用
- 微信开发的一款高效、小型的移动端key-value存储框架 MMKV 的使用
- Timber 日志打印框架的使用
- Kotlin 内联函数的使用
- Kotlin 泛型实化的使用
- Kotlin 作用域函数 takeUnless 的使用
结语
如果你不想使用 MMKV 进行缓存,你可以继承自 DataCache 接口,并改为你需要的缓存逻辑实现即可。
好啦,就写到这里吧 ,如果你想看更多的项目代码,请查看我在 Github 上的 阳光沙滩APP项目 ,你也可以在这里点击下载我写的 阳光沙滩APP客户端 进行体验。
如果对你有帮助的话,欢迎一键三连+关注哦~
本文由
A lonely cat
原创发布于
阳光沙滩
,未经作者授权,禁止转载