Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-19 14:18:24 +03:00
parent e2ce1e4a97
commit 584d99e95e
15 changed files with 168 additions and 46 deletions

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.TokensRepository
@ -22,5 +22,13 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
) = GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
): GetTokenListUseCase {
return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
return ToggleTokenListSortingUseCase(dispatchers)
}
}

View file

@ -9,18 +9,21 @@ import javax.inject.Inject
interface CoroutineDispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
val single: CoroutineDispatcher
}
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = Dispatchers.Main
override val io: CoroutineDispatcher = Dispatchers.IO
override val default: CoroutineDispatcher = Dispatchers.Default
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
class TestingCoroutineDispatcherProvider(
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
) : CoroutineDispatcherProvider

View file

@ -6,6 +6,7 @@ import arrow.core.raise.Raise
import arrow.core.raise.recover
import arrow.core.right
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TokenStatus
import com.tangem.domain.tokens.operations.TokenListOperations
@ -55,7 +56,7 @@ class GetTokenListUseCase(
refresh = refresh,
useCase = this@GetTokenListUseCase,
raise = this,
transformError = TokenListError::fromTokenStatusesOperations,
transformError = TokensStatusesOperations.Error::mapToTokenListError,
)
return operations.getTokensStatusesFlow()
@ -70,7 +71,7 @@ class GetTokenListUseCase(
tokens = tokens,
useCase = this@GetTokenListUseCase,
raise = this,
transform = TokenListError::fromTokenListOperations,
transform = TokenListOperations.Error::mapToTokenListError,
)
return operations.getTokenListFlow()

View file

@ -0,0 +1,66 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.operations.TokenListSortingOperations
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class ToggleTokenListSortingUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(tokenList: TokenList): Either<TokenListSortingError, TokenList> {
return withContext(dispatchers.default) {
either {
ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) {
TokenListSortingError.TokenListIsLoading
}
when (tokenList) {
is TokenList.GroupedByNetwork -> sortGroupedTokenList(tokenList)
is TokenList.Ungrouped -> sortUngroupedTokenList(tokenList)
is TokenList.NotInitialized -> raise(TokenListSortingError.TokenListIsEmpty)
}
}
}
}
private suspend fun Raise<TokenListSortingError>.sortGroupedTokenList(
tokenList: TokenList.GroupedByNetwork,
): TokenList.GroupedByNetwork {
val operations = getSortingOperations(tokenList)
val networks = tokenList.groups.map { it.network }.toSet()
return tokenList.copy(
groups = operations.getGroupedTokens(networks),
sortedBy = operations.getSortType(),
)
}
private suspend fun Raise<TokenListSortingError>.sortUngroupedTokenList(
tokenList: TokenList.Ungrouped,
): TokenList.Ungrouped {
val operations = getSortingOperations(tokenList)
return tokenList.copy(
tokens = operations.getTokens(),
sortedBy = operations.getSortType(),
)
}
private fun Raise<TokenListSortingError>.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> {
return TokenListSortingOperations(
tokenList = tokenList,
sortByBalance = tokenList.sortedBy != TokenList.SortType.BALANCE,
dispatchers = dispatchers,
raise = this,
transformError = TokenListSortingOperations.Error::mapToTokenListSortingError,
)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.domain.tokens.error
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.operations.TokensStatusesOperations
sealed class TokenListError {
@ -11,21 +9,4 @@ sealed class TokenListError {
data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : TokenListError()
data class DataError(val cause: Throwable) : TokenListError()
internal companion object {
fun fromTokenStatusesOperations(error: TokensStatusesOperations.Error): TokenListError = when (error) {
is TokensStatusesOperations.Error.DataError -> DataError(error.cause)
is TokensStatusesOperations.Error.EmptyNetworksStatuses,
is TokensStatusesOperations.Error.EmptyQuotes,
is TokensStatusesOperations.Error.EmptyTokens,
-> EmptyTokens
}
fun fromTokenListOperations(error: TokenListOperations.Error): TokenListError = when (error) {
is TokenListOperations.Error.DataError -> DataError(error.cause)
is TokenListOperations.Error.UnableToSortTokenList -> UnableToSortTokenList(error.unsortedTokenList)
is TokenListOperations.Error.UnableToGroupTokenList -> UnableToSortTokenList(error.ungroupedTokenList)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.tokens.error
sealed class TokenListSortingError {
object TokenListIsLoading : TokenListSortingError()
object TokenListIsEmpty : TokenListSortingError()
object UnableToSortTokenList : TokenListSortingError()
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.tokens.error.mapper
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.operations.TokensStatusesOperations
internal fun TokensStatusesOperations.Error.mapToTokenListError(): TokenListError {
return when (this) {
is TokensStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause)
is TokensStatusesOperations.Error.EmptyNetworksStatuses,
is TokensStatusesOperations.Error.EmptyQuotes,
is TokensStatusesOperations.Error.EmptyTokens,
-> TokenListError.EmptyTokens
}
}
internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError {
return when (this) {
is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause)
is TokenListOperations.Error.UnableToSortTokenList ->
TokenListError.UnableToSortTokenList(this.unsortedTokenList)
is TokenListOperations.Error.UnableToGroupTokenList ->
TokenListError.UnableToSortTokenList(this.ungroupedTokenList)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.tokens.error.mapper
import com.tangem.domain.tokens.error.TokenListSortingError
import com.tangem.domain.tokens.operations.TokenListSortingOperations
internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError {
return when (this) {
is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty
is TokenListSortingOperations.Error.EmptyNetworks,
is TokenListSortingOperations.Error.NetworkNotFound,
-> TokenListSortingError.UnableToSortTokenList
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.model
data class NetworkGroup(
val networkId: Network.ID,
val name: String,
val network: Network,
val tokens: Set<TokenStatus>,
)

View file

@ -51,7 +51,7 @@ internal class TokenListOperations<E>(
}
private suspend fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList {
return withContext(dispatchers.single) {
return withContext(dispatchers.default) {
val tokensNes = tokens.toNonEmptySetOrNull()
?: return@withContext TokenList.NotInitialized
@ -78,7 +78,7 @@ internal class TokenListOperations<E>(
val sortingOperations = TokenListSortingOperations(
tokens = tokens,
isAnyTokenLoading = isAnyTokenLoading,
isSortedByBalance = isSortedByBalance,
sortByBalance = isSortedByBalance,
dispatchers = dispatchers,
raise = this,
transformError = { e ->

View file

@ -17,20 +17,40 @@ import java.math.BigDecimal
internal class TokenListSortingOperations<E>(
private val tokens: Set<TokenStatus>,
private val isAnyTokenLoading: Boolean,
private val isSortedByBalance: Boolean,
private val sortByBalance: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
raise: Raise<E>,
transformError: (Error) -> E,
) : DelegatedRaise<TokenListSortingOperations.Error, E>(raise, transformError) {
constructor(
tokenList: TokenList,
dispatchers: CoroutineDispatcherProvider,
raise: Raise<E>,
transformError: (Error) -> E,
sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE,
isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading,
) : this(
tokens = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.tokens }.toSet()
is TokenList.Ungrouped -> tokenList.tokens
is TokenList.NotInitialized -> emptySet()
},
isAnyTokenLoading = isAnyTokenLoading,
sortByBalance = sortByBalance,
dispatchers = dispatchers,
raise = raise,
transformError = transformError,
)
suspend fun getGroupedTokens(networks: Set<Network>): NonEmptySet<NetworkGroup> {
return withContext(dispatchers.single) {
return withContext(dispatchers.default) {
ensure(tokens.isNotEmpty()) { Error.EmptyTokens }
val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) {
Error.EmptyNetworks
}
if (isSortedByBalance) {
if (sortByBalance) {
groupAndSortTokensByBalance(networksNes)
} else {
groupTokens(networksNes)
@ -39,16 +59,16 @@ internal class TokenListSortingOperations<E>(
}
suspend fun getTokens(): NonEmptySet<TokenStatus> {
return withContext(dispatchers.single) {
return withContext(dispatchers.default) {
val tokensNes = ensureNotNull(tokens.toNonEmptySetOrNull()) {
Error.EmptyTokens
}
if (isSortedByBalance) sortTokensByBalance(tokensNes) else tokensNes
if (sortByBalance) sortTokensByBalance(tokensNes) else tokensNes
}
}
fun getSortType() = if (isSortedByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE
fun getSortType() = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE
private fun groupTokens(networks: NonEmptySet<Network>): NonEmptySet<NetworkGroup> {
val groupedTokens = tokens
@ -59,8 +79,7 @@ internal class TokenListSortingOperations<E>(
}
NetworkGroup(
networkId = network.id,
name = network.name,
network = network,
tokens = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens },
)
}

View file

@ -15,7 +15,7 @@ internal class TokenStatusOperations(
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.single) {
suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.default) {
TokenStatus(
id = token.id,
networkId = token.networkId,

View file

@ -64,7 +64,7 @@ internal class TokensStatusesOperations<E>(
tokens: Set<Token>,
quotes: Set<Quote>,
networkStatuses: Set<NetworkStatus>,
): Set<TokenStatus> = withContext(dispatchers.single) {
): Set<TokenStatus> = withContext(dispatchers.default) {
tokens.mapTo(hashSetOf()) { token ->
val quote = quotes.firstOrNull { it.tokenId == token.id }
val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId }
@ -106,7 +106,7 @@ internal class TokensStatusesOperations<E>(
}
private suspend fun groupTokens(tokens: NonEmptySet<Token>): Map<Network.ID, NonEmptySet<Token.ID>> =
withContext(dispatchers.single) {
withContext(dispatchers.default) {
tokens
.groupBy { it.networkId }
.mapValues { (_, tokens) ->

View file

@ -14,24 +14,21 @@ internal object MockTokenLists {
const val isSortedByBalance = false
val networkGroup1 = NetworkGroup(
networkId = MockNetworks.network1.id,
name = MockNetworks.network1.name,
network = MockNetworks.network1,
tokens = MockTokensStates.tokenStates
.filter { it.networkId == MockNetworks.network1.id }
.toNonEmptySetOrNull()!!,
)
val networkGroup2 = NetworkGroup(
networkId = MockNetworks.network2.id,
name = MockNetworks.network2.name,
network = MockNetworks.network2,
tokens = MockTokensStates.tokenStates
.filter { it.networkId == MockNetworks.network2.id }
.toNonEmptySetOrNull()!!,
)
val networkGroup3 = NetworkGroup(
networkId = MockNetworks.network3.id,
name = MockNetworks.network3.name,
network = MockNetworks.network3,
tokens = MockTokensStates.tokenStates
.filter { it.networkId == MockNetworks.network3.id }
.toNonEmptySetOrNull()!!,
@ -42,7 +39,7 @@ internal object MockTokenLists {
val sortedNetworksGroups = networksGroups.map { group ->
group.copy(
tokens = MockTokensStates.loadedTokensStates
.filter { it.networkId == group.networkId }
.filter { it.networkId == group.network.id }
.sortedByDescending { it.value.fiatAmount }
.toNonEmptySetOrNull()!!,
)

View file

@ -44,7 +44,7 @@ internal class TokenListToContentItemsConverter(
}
private fun MutableList<MultiCurrencyItem>.addGroup(group: NetworkGroup): List<MultiCurrencyItem> {
this.add(MultiCurrencyItem.NetworkGroupTitle(group.name))
this.add(MultiCurrencyItem.NetworkGroupTitle(group.network.name))
group.tokens.forEach { token ->
this.addToken(token)