Updated on 2026-08-14
This commit is contained in:
parent
c1561a366c
commit
c6fc0d49ba
10 changed files with 135 additions and 33 deletions
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue