Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-27 17:08:13 +03:00
parent c1561a366c
commit c6fc0d49ba
10 changed files with 135 additions and 33 deletions

View file

@ -1,15 +1,11 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.quote.DefaultQuotesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.quote.model.StoredQuote
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -19,13 +15,9 @@ internal object QuotesStoreModule {
@Provides
@Singleton
fun provideQuotesStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): QuotesStore {
fun provideQuotesStore(): QuotesStore {
return DefaultQuotesStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "quotes",
context = context,
adapter = moshi.adapter(StoredQuote::class.java),
),
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -17,6 +17,14 @@ internal class FileDataStore<Value : Any>(
) : StringKeyDataStore<Value> {
private val writeTrigger = Trigger()
override suspend fun isEmpty(): Boolean {
val e = NotImplementedError("`isEmpty()` function not implemented for `FileDataStore`")
Timber.e(e)
throw e
}
override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null
override fun get(key: String): Flow<Value> {
return writeTrigger

View file

@ -11,6 +11,10 @@ internal class RuntimeDataStore<Data : Any> : StringKeyDataStore<Data> {
store.tryEmit(value = null)
}
override suspend fun isEmpty(): Boolean = store.firstOrNull().isNullOrEmpty()
override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null
override fun get(key: String): Flow<Data> {
return store.mapNotNull { value ->
value?.get(key)

View file

@ -26,6 +26,14 @@ internal abstract class SharedPreferencesDataStore<Value : Any>(
abstract fun storeByKey(key: String, value: Value)
override suspend fun isEmpty(): Boolean {
return sharedPreferences.all.isEmpty()
}
override suspend fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
override fun get(key: String): Flow<Value> {
return writeTrigger
.mapNotNull { getInternal(key) }

View file

@ -2,23 +2,94 @@ package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
/**
* Represents a generic key-value data store.
*
* @param Key The type of the keys used to identify values in the store.
* @param Value The type of the values stored.
*/
internal interface DataStore<Key : Any, Value : Any> {
/**
* Checks if the data store is empty.
*
* @return `true` if the data store has no entries, otherwise `false`.
*/
suspend fun isEmpty(): Boolean
/**
* Checks if the data store contains an entry with the specified key.
*
* @param key The key to check for presence in the store.
* @return `true` if the key is present, otherwise `false`.
*/
suspend fun contains(key: Key): Boolean
/**
* Retrieves a value updates associated with the given key, as a flow.
*
* @param key The key to look up in the store.
* @return A flow emitting the value associated with the given key.
*/
fun get(key: Key): Flow<Value>
/**
* Retrieves all values updates from the data store, as a flow.
*
* @return A flow emitting a list of all values in the store.
*/
fun getAll(): Flow<List<Value>>
/**
* Retrieves a value associated with the given key synchronously.
*
* If the key does not exist, this method returns `null`.
*
* @param key The key to look up in the store.
* @return The value associated with the key, or `null` if not present.
*/
suspend fun getSyncOrNull(key: Key): Value?
/**
* Retrieves all values from the data store synchronously.
*
* If the store is empty, this method returns `null`.
*
* @return A list of all values in the store, or `null` if empty.
*/
suspend fun getAllSyncOrNull(): List<Value>?
/**
* Stores a value in the data store associated with the given key.
*
* @param key The key to associate with the value.
* @param value The value to store.
*/
suspend fun store(key: Key, value: Value)
/**
* Stores multiple values in the data store with their associated keys.
*
* @param values A map of keys to values to store.
*/
suspend fun store(values: Map<Key, Value>)
/**
* Removes a value associated with the given key from the data store.
*
* @param key The key of the value to remove.
*/
suspend fun remove(key: Key)
/**
* Removes values associated with the given keys from the data store.
*
* @param keys Keys of values to remove.
*/
suspend fun remove(keys: Collection<Key>)
/**
* Clears all entries from the data store.
*/
suspend fun clear()
}

View file

@ -13,7 +13,7 @@ internal abstract class KeylessDataStoreDecorator<Value : Any>(
open suspend fun store(item: Value) = store(key, item)
open suspend fun isEmpty() = getSyncOrNull() == null
override suspend fun isEmpty(): Boolean = getSyncOrNull() == null
private companion object {
const val DEFAULT_STRING_KEY = "key"

View file

@ -8,6 +8,10 @@ internal abstract class StringKeyDataStoreDecorator<Key : Any, Value : Any>(
abstract fun provideStringKey(key: Key): String
override suspend fun isEmpty(): Boolean = wrappedDataStore.isEmpty()
override suspend fun contains(key: Key): Boolean = wrappedDataStore.contains(provideStringKey(key))
override fun get(key: Key): Flow<Value> {
return wrappedDataStore.get(provideStringKey(key))
}

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
internal class DefaultQuotesStore(
@ -12,16 +13,24 @@ internal class DefaultQuotesStore(
) : QuotesStore {
override fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>> {
val flows = currenciesIds.mapNotNull { currencyId ->
dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null)
}
return channelFlow {
val flows = currenciesIds.mapNotNull { currencyId ->
currencyId.rawCurrencyId?.let(dataStore::get)
}
return combine(flows) { quotes -> quotes.toSet() }
if (dataStore.isEmpty() || flows.isEmpty()) {
send(emptySet())
}
combine(flows) { quotes -> quotes.toSet() }.collect(::send)
}
}
override suspend fun store(response: QuotesResponse) {
response.quotes.forEach { (rawCurrencyId, quote) ->
dataStore.store(rawCurrencyId, StoredQuote(rawCurrencyId, quote))
val quotes = response.quotes.mapValues { (id, quote) ->
StoredQuote(id, quote)
}
dataStore.store(quotes)
}
}

View file

@ -99,15 +99,17 @@ internal class CurrenciesStatusesOperations(
val quoteFlow = getQuotes(currenciesIds)
.map { maybeQuotes ->
maybeQuotes.map { quotes ->
quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
maybeQuotes.flatMap { quotes ->
quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right()
?: Error.EmptyQuotes.left()
}
}
val statusFlow = getNetworksStatuses(networks)
.map { maybeStatuses ->
maybeStatuses.map { statuses ->
statuses.singleOrNull { it.network == currency.network }
maybeStatuses.flatMap { statuses ->
statuses.singleOrNull { it.network == currency.network }?.right()
?: Error.EmptyNetworksStatuses.left()
}
}

View file

@ -9,10 +9,7 @@ import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.mock.MockTokensStates
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
@ -24,6 +21,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
@ -113,9 +111,17 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
}
@Test
fun `when quotes are empty and statuses are verified then loading token should be received`() = runTest {
val expectedResult = MockTokensStates.tokenState1
.copy(value = CryptoCurrencyStatus.Loading)
fun `when quotes are empty and statuses are verified then token without quote should be received`() = runTest {
val expectedResult = with(MockTokensStates.tokenState1) {
copy(
value = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal.TEN,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(defaultAddress = "mock"),
),
)
}
.right()
val useCase = getUseCase(
@ -131,10 +137,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
}
@Test
fun `when quotes are loaded and statuses are empty then loading token should be received`() = runTest {
val expectedResult = MockTokensStates.tokenState1
.copy(value = CryptoCurrencyStatus.Loading)
.right()
fun `when quotes are loaded and statuses are empty then error be received`() = runTest {
val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left()
val useCase = getUseCase(
statuses = flowOf(emptySet<NetworkStatus>().right()),