Simple in-memory cache conception built on Map
.
Add the dependency:
repositories {
jcenter()
}
dependencies {
implementation("com.redmadrobot.mapmemory:mapmemory:1.0")
// or if you want to work with memory in reactive style, add one of
implementation("com.redmadrobot.mapmemory:mapmemory-rxjava2:1.0")
implementation("com.redmadrobot.mapmemory:mapmemory-coroutines:1.0")
}
Kotlin provides delegates for access to map entries. This library exploits and improves this idea to implement in-memory storage.
There are two simple principles:
- Memory is a singleton, and it can be shared between many consumers
- Memory holds data but not knows what data it holds
class CardsInMemoryStorage(memory: MapMemory) {
private val cards: MutableMap<String, Card> by memory.map()
operator fun get(id: String): Card? = cards[id]
operator fun set(id: String, card: Card) {
cards[id] = card
}
}
There are default accessors available:
Accessor | Default value | Description |
---|---|---|
nullable() |
null |
Store nullable values |
map() |
Empty map | Store values in map |
list() |
Empty list | Store values in list |
You can create own accessor if need.
You, also, can use delegate without any functions:
var unsafeValue: String by memory
Be careful, there no default values, and you will get NoSuchElementException
if you try read value before it was written.
To use MapMemory
in reactive style, replace dependency mapmemory
with mapmemory-rxjava2
or mapmemory-coroutines
.
You can't use both map
mapmemory-rxjava2
andmapmemory-coroutines
at the same time because you will get duplicates in classpath.
Both of reactive implementations contain RactiveMap
.
ReactiveMap
works similar to MutableMap
but enables you to observe data in reactive manner.
It has methods getStream(key)
and getAllStream()
to observe one or all map values accordingly.
With reactive in-memory cache you will always have actual data on screen. Also, you can separate subscription to data and request of data to manage it easier.
mapmemory-rxjava2
adds accessors for RxJava types:
Accessor | Default value | Description |
---|---|---|
behaviorSubject() |
Empty subject | Store stream of values |
publishSubject() |
Empty subject | Store stream of values |
maybe() |
Maybe.empty() |
Reactive analog to store "nullable" |
reactiveMap() |
Empty map | Store values in reactive map |
Example of cache-first approach with reactive subscription:
class CardsRepository(memory: MapMemory) {
private val cardsCache: ReactiveCache<Card> by memory.reactiveCache()
/** Returns observable for cards in cache. */
fun getCardsStream(): Observable<List<Card>> = cardsCache.getAllStream()
/** Updates cards in cache. */
fun fetchCards(): Completable {
return Single.defer {/* get cards from network */}
.doOnSuccess { cardsCache.replaceAll(it) }
.ignoreResult()
}
}
class CardsViewModel(private val repository: CardsRepository) : ViewModel() {
init {
// Subscribe to cache
repository.getCardsStream()
.subscribe {/* update cards on screen */}
}
fun onRefresh() {
repository.fetchCards()
.subscribe(
{/* success! hide progress */},
{/* fail. hide progress and show error */}
)
}
}
mapmemory-coroutines
adds accessors for coroutines types:
Accessor | Default value | Description |
---|---|---|
stateFlow() |
StateFlow with specified initialValue |
Store stream of values |
sharedFlow() |
Empty flow | Store stream of values |
reactiveMap() |
Empty map | Store values in reactive map |
Example of cache-first approach with reactive subscription:
class CardsRepository(private val api: CardsApi, memory: MapMemory) {
private val cardsCache: ReactiveCache<Card> by memory.reactiveCache()
/** Returns flow for cards in cache. */
fun getCardsStream(): Flow<List<Card>> = cardsCache.getAllStream()
/** Updates cards in cache. */
suspend fun fetchCards() {
val cards = api.getCards()
cardsCache.replaceAll(cards.associateBy { it.id })
}
}
class CardsViewModel(private val repository: CardsRepository) : ViewModel() {
init {
// Subscribe to cache
repository.getCardsStream()
.onEach {/* update cards on screen */}
.launchIn(viewModelScope)
}
fun onRefresh() {
viewModelScope.launch {
repository.fetchCards()
// success! hide progress
}
}
}
May be useful to create memory scopes. You can use it to control data lifetime.
/** Memory, available during a session and cleared on logout. */
@Singleton
class SessionMemory @Inject constructor() : MapMemory()
/** Memory, available during the app lifetime. */
@Singleton
class AppMemory @Inject constructor() : MapMemory()
Keep in mind that you should manually clear SessionMemory
on logout.
Instead of creating subclass, you can provide MapMemory with qualifiers.
It can be caused if you have declared fields with the same name but different types in several places.
Note that field name used as a key to access a value in MapMemory
.
To avoid ClassCastException
, use unique names for fields.
This snippet demonstrates how it works:
class StringsStorage(memory: MapMemory) {
var values: MutableList<String> by memory.list()
}
class IntsStorage(memory: MapMemory) {
var values: MutableList<Int> by memory.list() // The same name as in StringsStorage
}
val strings = StringsStorage(memory)
val ints = IntsStorage(memory)
strings.values.add("A")
ints.values.add(1)
println(memory["values"]) // [A, 1]
Merge requests are welcome. For major changes, please open an issue first to discuss what you would like to change.