轻松缓存 Android + Kotlin + Flow
技术背景
在某些情况下,良好的缓存机制可以帮助您作为开发人员并提高应用程序的质量。让我简要介绍一下其中的 2 个,然后介绍简单的解决方案。
案例1
假设您正在开发一个具有几个屏幕和多个 API 调用的 Android 应用程序。随着时间的推移,具有后台逻辑的屏幕和组件越来越多。因此,一段时间后我们几乎同时在应用程序中发生 2 个或更多类似 API GET 的调用是很常见的。有时它会发生在敏感的地方,比如应用程序启动。
对用户意味着什么?显然这是额外的加载时间和电池使用量。
案例2
您正在实现一项需要网络或数据库请求才能显示某些信息的功能。可能是配置文件数据或操作历史记录。您正在考虑如何处理可能的加载错误。通常你会显示一些错误信息。也许您甚至会显示重试按钮。但是,如果用户遇到网络连接问题怎么办?有时最好同时显示一些过时的信息(如灰色)和前面提到的选项。
案例N
我相信您可以想象更多的情况,当易于使用的缓存可以改进您的应用程序并节省您的精力时。
缓存实现概述
通用缓存
因此,让我介绍一下经过一些迭代后我满意的方式。我想实现一些核心原则。
便于使用
只需用几行代码包装您的数据流,然后像往常一样或以高级方式使用它。它会将结果自动缓存到内存或您自定义的任何地方。
val cachedSource = CachedSource<String, Any>(
source = {
params -> api.getSomething(params) }
)
还有一些默认参数需要根据实际需求进行调整
class CachedSource<P : Any, T : Any>(
source: suspend (params: P) -> T,
private val cache: Cache<P, T> = MemoryCache(1),
private val timeProvider: TimeProvider = SystemTimeProvider,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
)
获取CachedSource
中的Flow:
cachedSource.get("params", FromCache.IF_HAVE, maxAge = 5_000)
.collect {
// Use received values
}
持续的请求共享
Ongoing request sharing
可选。通常我们可以假设不需要额外的并行请求。但有时仍然需要在不与正在进行的请求共享的情况下提出专门的请求,以确保我们获得最新信息。
此外,我们不仅需要根据数据来源来区分请求,还需要根据请求的确切参数来区分请求。
所以你可以共享来自“…endpoint1/?param=A” 的数据,但是不要共享来自“…endpoint1/?param=B”的数据。
suspend fun get(
...
shareOngoingRequest: Boolean = true,
...
): Flow<T> =
不同的请求选项
基本上,当您加载信息时,您可能需要几个主要行为:
- 始终从真实请求中获取数据。从不从缓存中获取
- 从真实请求中获取,但如果失败则使用缓存
- 从缓存中获取,但如果没有合适的缓存则使用真实请求
- 仅从缓存中获取(如果为空则出错)
- 从缓存中获取(快速)然后从真实请求中获取(长)
enum class FromCache {
NEVER,
IF_FAILED,
IF_HAVE,
ONLY,
CACHED_THEN_LOAD,
}
从结果中获取与缓存相关的信息
所以你总是可以检查它是来自缓存的数据还是刚刚加载的数据。如果来自缓存,它有多大。根据此信息,您可以根据需要手动切换到新请求。因此,您可以根据需要进一步自定义生成的流程。
data class CachedSourceResult<T : Any>(
val value: T,
val fromCache: Boolean,
val originTimeStamp: Long?,
)
其他
当然还有一些其他有用的功能,例如重置缓存或侦听(通过 Flow)任何更新的能力。如果你想实现一些不常见的行为,你也可以以flatMap 的方式组合流。
例如,这里有一个扩展,用于检查自定义条件的缓存并在没有缓存或条件未通过时启动真正的请求:
suspend fun <P : Any, T : Any> CachedSource<P, T>.getOrRequest(
params: P,
fromCachePredicate: (cached: CachedSourceResult<T>) -> Boolean,
): Flow<T> =
flow {
getRaw(params, FromCache.IF_HAVE)
.collect {
if (!it.fromCache || fromCachePredicate(it)) {
emit(it.value)
} else {
emitAll(
getRaw(params, FromCache.NEVER)
.map {
result -> result.value }
)
}
}
}
像下面的方式发起调用:
source
.getOrRequest( "param" , fromCachePredicate = {
it.someData.isNotEmpty() })
.collect {
... }
最后,附上github相关实现的代码:
https://github.com/Andrew0000/Universal-Cache