Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-12 23:05:07 +04:00
parent 4f5257e537
commit 777a05d716
8 changed files with 112 additions and 79 deletions

View file

@ -1,12 +1,20 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.quote.DefaultQuotesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.quote.utils.QuotesSerializer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -15,9 +23,17 @@ internal object QuotesStoreModule {
@Provides
@Singleton
fun provideQuotesStore(): QuotesStore {
fun provideQuotesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): QuotesStore {
return DefaultQuotesStore(
dataStore = RuntimeDataStore(),
dataStore = DataStoreFactory.create(
serializer = QuotesSerializer(moshi),
produceFile = { context.dataStoreFile(fileName = "quotes") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
)
}
}

View file

@ -1,42 +1,55 @@
package com.tangem.datasource.local.quote
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.datasource.local.quote.model.QuoteDM
import com.tangem.datasource.local.quote.model.QuotesDM
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.*
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
internal class DefaultQuotesStore(
private val dataStore: StringKeyDataStore<StoredQuote>,
private val dataStore: DataStore<QuotesDM>,
) : QuotesStore {
override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<StoredQuote>> {
return channelFlow {
val flows = currenciesIds.map { currencyId -> dataStore.get(currencyId.value) }
override fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>> {
return dataStore.data
.map { quotes -> createQuotes(currenciesIds, quotes) }
}
if (dataStore.isEmpty() || flows.isEmpty()) {
send(emptySet())
}
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote> {
val quotes = dataStore.data.firstOrNull().orEmpty()
merge(*flows.toTypedArray())
.scan<StoredQuote, Set<StoredQuote>>(emptySet()) { acc, quote ->
acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId }
return createQuotes(currenciesIds, quotes)
}
private fun createQuotes(currenciesIds: Set<CryptoCurrency.RawID>, quoteEntities: Set<QuoteDM>): Set<Quote> =
currenciesIds.mapTo(mutableSetOf()) { id ->
quoteEntities.firstOrNull { it.rawCurrencyId == id }
?.let { quote ->
Quote.Value(
rawCurrencyId = id,
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
)
}
.filter(Set<StoredQuote>::isNotEmpty)
.collect(::send)
?: Quote.Empty(id)
}
}
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<StoredQuote> {
return currenciesIds.mapNotNull { currencyId -> dataStore.getSyncOrNull(currencyId.value) }.toSet()
}
override suspend fun store(response: QuotesResponse) {
val quotes = response.quotes.mapValues { (id, quote) ->
StoredQuote(id, quote)
val newQuotes = response.quotes.mapTo(mutableSetOf()) { (currencyId, quote) ->
QuoteDM(
rawCurrencyId = CryptoCurrency.RawID(currencyId),
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
)
}
dataStore.store(quotes)
dataStore.updateData { storedQuotes ->
(newQuotes + storedQuotes).distinctBy { it.rawCurrencyId }.toSet()
}
}
}

View file

@ -1,15 +1,15 @@
package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import kotlinx.coroutines.flow.Flow
interface QuotesStore {
fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<StoredQuote>>
fun get(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>>
suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<StoredQuote>
suspend fun getSync(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>
suspend fun store(response: QuotesResponse)
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.local.quote.model
import com.squareup.moshi.JsonClass
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
internal typealias QuotesDM = Set<QuoteDM>
@JsonClass(generateAdapter = true)
internal data class QuoteDM(
val rawCurrencyId: CryptoCurrency.RawID,
val fiatRate: BigDecimal,
val priceChange: BigDecimal,
)

View file

@ -1,9 +0,0 @@
package com.tangem.datasource.local.quote.model
import com.squareup.moshi.Json
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
data class StoredQuote(
@Json(name = "rawCurrencyId") val rawCurrencyId: String,
@Json(name = "quote") val quote: QuotesResponse.Quote,
)

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.local.quote.utils
import androidx.datastore.core.Serializer
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.local.quote.model.QuoteDM
import com.tangem.datasource.local.quote.model.QuotesDM
import java.io.InputStream
import java.io.OutputStream
internal class QuotesSerializer(moshi: Moshi) : Serializer<QuotesDM> {
private val adapter by lazy {
val types = Types.newParameterizedType(Set::class.java, QuoteDM::class.java)
moshi.adapter<QuotesDM>(types)
}
override val defaultValue: QuotesDM = emptySet()
override suspend fun readFrom(input: InputStream): QuotesDM {
return input.bufferedReader().use { reader ->
adapter.fromJson(reader.readText()) ?: defaultValue
}
}
override suspend fun writeTo(t: QuotesDM, output: OutputStream) {
output.bufferedWriter().use { write ->
write.write(adapter.toJson(t))
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.data.tokens.repository
import com.tangem.data.common.api.safeApiCallWithTimeout
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.QuotesConverter
import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
@ -29,7 +28,6 @@ internal class DefaultQuotesRepository(
private val dispatchers: CoroutineDispatcherProvider,
) : QuotesRepository {
private val quotesConverter = QuotesConverter()
private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter()
@Volatile
@ -37,7 +35,8 @@ internal class DefaultQuotesRepository(
private val mutex = Mutex()
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.RawID>): Flow<Set<Quote>> {
TODO("Will be implemented in [REDACTED_TASK_KEY]")
return quotesStore.get(currenciesIds)
.flowOn(dispatchers.io)
}
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean) {
@ -62,7 +61,7 @@ internal class DefaultQuotesRepository(
.filterNotNull()
.flatMapLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh)
quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) }
quotesStore.get(currenciesIds)
}
.cancellable()
.flowOn(dispatchers.io)
@ -79,17 +78,15 @@ internal class DefaultQuotesRepository(
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh)
val quotes = quotesStore.getSync(currenciesIds)
quotesConverter.convert(currenciesIds to quotes)
quotesStore.getSync(currenciesIds)
}
}
override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote? {
return withContext(dispatchers.io) {
val setOfCurrencyId = setOf(currencyId)
val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull()
quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() }
quotesStore.getSync(setOfCurrencyId).firstOrNull()
}
}
@ -134,6 +131,7 @@ internal class DefaultQuotesRepository(
response,
replacementIdsResult.idsFiltered,
)
quotesStore.store(updatedResponse)
}

View file

@ -1,31 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.datasource.local.quote.model.StoredQuote
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
typealias QuotesConverterValue = Pair<Set<CryptoCurrency.RawID>, Set<StoredQuote>>
internal class QuotesConverter : Converter<QuotesConverterValue, Set<Quote>> {
override fun convert(value: QuotesConverterValue): Set<Quote> {
val (setOfCurrencyId, setOfStoredQuote) = value
return setOfCurrencyId.mapTo(hashSetOf()) { id ->
setOfStoredQuote.find { id.value == it.rawCurrencyId }
?.let(::convertExistStoredQuote)
?: Quote.Empty(id)
}
}
private fun convertExistStoredQuote(storedQuote: StoredQuote): Quote {
val (rawCurrencyId, responseQuote) = storedQuote
return Quote.Value(
rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId),
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
)
}
}