Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-30 08:52:15 +01:00
parent 028b21b112
commit 348d750054
17 changed files with 698 additions and 300 deletions

View file

@ -13,20 +13,13 @@ import com.tangem.domain.news.model.NewsListBatchFlow
import com.tangem.domain.news.model.NewsListBatchingContext
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.*
import com.tangem.pagination.exception.EndOfPaginationException
import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
/**
@ -42,12 +35,14 @@ internal class DefaultNewsRepository(
) : NewsRepository {
override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow {
return BatchListSource(
val newsBatchFlow = BatchListSource(
fetchDispatcher = dispatchers.io,
context = context,
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: INITIAL_BATCH_KEY },
batchFetcher = createBatchFetcher(batchSize),
).toBatchFlow()
return updateViewedStatusForNewsBatch(newsBatchFlow, context.coroutineScope)
}
override suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle> {
@ -115,18 +110,52 @@ internal class DefaultNewsRepository(
}
override suspend fun getCategories(): List<ArticleCategory> {
return newsApi.getCategories().getOrThrow().items.map { dto ->
return withContext(dispatchers.io) {
newsApi.getCategories().getOrThrow().items.map { dto ->
ArticleCategory(
id = dto.id,
name = dto.name,
)
}
}
}
override suspend fun updateNewsViewed(articleIds: Collection<Int>, viewed: Boolean) {
newsViewedStore.updateViewed(articleIds, viewed)
}
private fun updateViewedStatusForNewsBatch(
newsBatchFlow: NewsListBatchFlow,
scope: CoroutineScope,
): NewsListBatchFlow {
return object : NewsListBatchFlow {
override val state: StateFlow<BatchListState<Int, List<ShortArticle>>> =
combine(
newsBatchFlow.state,
newsViewedStore.getAll(),
) { batchListState, viewedFlags ->
val updatedBatches = batchListState.data.map { batch ->
val updatedArticles = batch.data.map { article ->
val isViewed = viewedFlags[article.id] == true
article.copy(viewed = isViewed)
}
Batch(key = batch.key, data = updatedArticles)
}
BatchListState(
data = updatedBatches,
status = batchListState.status,
)
}.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = BatchListState(emptyList(), newsBatchFlow.state.value.status),
)
override val updateResults: SharedFlow<Pair<Nothing, BatchUpdateResult<Int, List<ShortArticle>>>> =
newsBatchFlow.updateResults
}
}
private suspend fun fetchDetailedArticlesInternal(newsIds: Collection<Int>, language: String?) =
withContext(dispatchers.io) {
if (newsIds.isEmpty()) return@withContext
@ -141,7 +170,7 @@ internal class DefaultNewsRepository(
if (idsToFetch.isEmpty()) return@withContext
val fetchedArticles = coroutineScope {
val fetchedArticles = supervisorScope {
idsToFetch.map { newsId ->
async {
newsApi.getNewsDetails(newsId = newsId, language = language)
@ -160,13 +189,12 @@ internal class DefaultNewsRepository(
private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) {
return withContext(dispatchers.io) {
val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)
when (val result = apiResponse) {
when (val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)) {
is ApiResponse.Error -> {
Timber.e(
result.cause.cause,
apiResponse.cause.cause,
"Trending news fetch failed cause: ${
when (val error = result.cause) {
when (val error = apiResponse.cause) {
is ApiResponseError.HttpException -> error.code
is ApiResponseError.NetworkException -> "NetworkException"
is ApiResponseError.TimeoutException -> "TimeoutException"
@ -179,14 +207,14 @@ internal class DefaultNewsRepository(
key = TRENDING_NEWS_KEY,
value = TrendingNews.Error(
NewsError.Unknown(
message = result.cause.message,
message = apiResponse.cause.message,
code = null,
),
),
)
}
is ApiResponse.Success<NewsTrendingResponse> -> {
val freshArticles = result.data.items.map { it.toDomainShortArticle() }
val freshArticles = apiResponse.data.items.map { it.toDomainShortArticle() }
val articles = freshArticles.take(limit)
trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(articles))
TrendingNews.Data(articles)
@ -271,7 +299,7 @@ internal class DefaultNewsRepository(
page = page,
limit = limit,
language = params.language,
snapshot = snapshotOverride,
snapshot = snapshotOverride?.takeIf { it.isNotEmpty() },
tokenIds = params.tokenIds.takeIf { it.isNotEmpty() },
categoryIds = params.categoryIds.takeIf { it.isNotEmpty() },
).getOrThrow()

View file

@ -78,7 +78,12 @@ internal class FeedEntryChildFactory @Inject constructor(
DefaultNewsListComponent(
appComponentContext = appComponentContext,
params = DefaultNewsListComponent.Params(
onArticleClicked = { feedEntryClickIntents.onArticleClick(articleId = it) },
onArticleClicked = { currentArticle, prefetchedArticles ->
feedEntryClickIntents.onArticleClick(
articleId = currentArticle,
preselectedArticlesId = prefetchedArticles,
)
},
onBackClick = onBackClicked,
),
)

View file

@ -51,7 +51,7 @@ internal class DefaultNewsListComponent(
@Serializable
data class Params(
val onArticleClicked: (Int) -> Unit,
val onArticleClicked: (currentArticle: Int, prefetchedArticles: List<Int>) -> Unit,
val onBackClick: () -> Unit,
)
}

View file

@ -0,0 +1,98 @@
package com.tangem.features.feed.model.converter
import com.tangem.pagination.Batch
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
internal class BatchListStateManager<Key, Domain, UI>(
private val converter: BatchItemConverter<Domain, UI>,
private val dispatchers: CoroutineDispatcherProvider,
) {
val state = MutableStateFlow(BatchListState<Key, Domain, UI>())
suspend fun update(newList: List<Batch<Key, List<Domain>>>, forceUpdate: Boolean) =
withContext(dispatchers.default) {
state.update { currentState ->
val uiBatches = currentState.uiBatches
val previousList = currentState.processedItems
if (newList.isEmpty()) {
return@update BatchListState(uiBatches = emptyList(), processedItems = emptyList())
}
val isInitialLoading = forceUpdate ||
previousList.isNullOrEmpty() ||
newList.firstOrNull()?.key != previousList.firstOrNull()?.key
val outItems = if (isInitialLoading) {
newList.map { batch ->
Batch(key = batch.key, data = batch.data.map { converter.convert(it) })
}
} else {
if (previousList.size != newList.size) {
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
val newBatches = newList.filter { keysToAdd.contains(it.key) }
uiBatches + newBatches.map { batch ->
Batch(key = batch.key, data = batch.data.map { converter.convert(it) })
}
} else {
uiBatches.mapIndexed { batchIndex, batch ->
val prevBatch = previousList[batchIndex]
val newBatch = newList[batchIndex]
if (prevBatch == newBatch) return@mapIndexed batch
Batch(
key = batch.key,
data = batch.data.mapIndexed { index, currentUiItem ->
val prevItem = prevBatch.data.getOrNull(index)
val newItem = newBatch.data.getOrNull(index)
if (prevItem != null && newItem != null) {
converter.update(prevItem, currentUiItem, newItem)
} else if (newItem != null) {
converter.convert(newItem)
} else {
currentUiItem
}
},
)
}
}
}
coroutineContext.ensureActive()
BatchListState(
uiBatches = outItems,
processedItems = newList,
)
}
}
}
internal data class BatchListState<K, Domain, UI>(
val uiBatches: List<Batch<K, List<UI>>> = emptyList(),
val processedItems: List<Batch<K, List<Domain>>>? = emptyList(),
)
internal interface BatchItemConverter<Domain, UI> {
fun convert(item: Domain): UI
fun update(prevDomain: Domain, currentUI: UI, newDomain: Domain): UI {
return convert(newDomain)
}
}
internal fun <K, T> Flow<List<Batch<K, List<T>>>>.distinctBatchesContent(): Flow<List<Batch<K, List<T>>>> {
return this.distinctUntilChanged { old, new ->
old.size == new.size &&
old.map { it.key } == new.map { it.key } &&
old.flatMap { it.data } == new.flatMap { it.data }
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.feed.model.market.details.converter
package com.tangem.features.feed.model.converter
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
@ -8,12 +8,16 @@ import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle
import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
internal class ShortArticleToArticleConfigUMConverter(
private val isTrending: Provider<Boolean>,
) : Converter<List<ShortArticle>, ImmutableList<ArticleConfigUM>> {
override fun convert(value: List<ShortArticle>): ImmutableList<ArticleConfigUM> {
return value.map { shortArticle ->
@ -21,7 +25,7 @@ class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<Article
id = shortArticle.id,
title = shortArticle.title,
score = shortArticle.score,
isTrending = false,
isTrending = isTrending(),
tags = buildArticleTags(shortArticle),
createdAt = mapFormattedDate(shortArticle.createdAt),
isViewed = shortArticle.viewed,
@ -29,7 +33,7 @@ class RelatedNewsConverter : Converter<List<ShortArticle>, ImmutableList<Article
}.toPersistentList()
}
private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet<LabelUM> {
private fun buildArticleTags(article: ShortArticle): ImmutableSet<LabelUM> {
val categoryLabels = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}

View file

@ -16,6 +16,8 @@ import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager
import com.tangem.features.feed.model.feed.state.TrendingNewsStateFactory
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.utils.Provider

View file

@ -1,13 +1,12 @@
package com.tangem.features.feed.ui.feed.state
package com.tangem.features.feed.model.feed.state
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
import com.tangem.features.feed.model.converter.*
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.PaginationStatus
@ -18,8 +17,11 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class FeedMarketsBatchFlowManager(
@ -44,7 +46,7 @@ internal class FeedMarketsBatchFlowManager(
}
}.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = emptyMap(),
)
@ -60,7 +62,7 @@ internal class FeedMarketsBatchFlowManager(
}
}.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = emptyMap(),
)
@ -76,7 +78,7 @@ internal class FeedMarketsBatchFlowManager(
}
}.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = emptyMap(),
)
@ -141,11 +143,29 @@ internal class FeedMarketsBatchFlowManager(
private val dispatchers: CoroutineDispatcherProvider,
) {
private val updateStateJob = JobHolder()
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = resultBatches.map { it.uiBatches }
private val batchConverter = object : BatchItemConverter<TokenMarket, MarketsListItemUM> {
private val internalConverter: MarketsTokenItemConverter
get() = MarketsTokenItemConverter(
currentTrendInterval = MarketsListUM.TrendInterval.H24,
appCurrency = currentAppCurrency(),
)
override fun convert(item: TokenMarket) = internalConverter.convert(item)
override fun update(prevDomain: TokenMarket, currentUI: MarketsListItemUM, newDomain: TokenMarket) =
internalConverter.update(prevDomain, currentUI, newDomain)
}
private val stateManager = BatchListStateManager<Int, TokenMarket, MarketsListItemUM>(
converter = batchConverter,
dispatchers = dispatchers,
)
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>> =
uiBatches
stateManager.state
.map { it.uiBatches }
.map { batches ->
batches.asSequence()
.map { it.data }
@ -155,7 +175,7 @@ internal class FeedMarketsBatchFlowManager(
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = persistentListOf(),
)
@ -170,7 +190,7 @@ internal class FeedMarketsBatchFlowManager(
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = false,
)
@ -185,7 +205,7 @@ internal class FeedMarketsBatchFlowManager(
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Companion.Eagerly,
started = Eagerly,
initialValue = false,
)
@ -210,15 +230,11 @@ internal class FeedMarketsBatchFlowManager(
init {
batchFlow.state
.map { it.data }
.distinctUntilChanged { a, b ->
a.size == b.size &&
a.map { it.key } == b.map { it.key } &&
a.map { it.data }.flatten() == b.map { it.data }.flatten()
}
.onEach {
.distinctBatchesContent()
.onEach { newList ->
coroutineScope {
launch {
updateState(it)
stateManager.update(newList = newList, forceUpdate = false)
}.saveIn(updateStateJob)
}
}
@ -226,77 +242,9 @@ internal class FeedMarketsBatchFlowManager(
.launchIn(modelScope)
}
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
withContext(dispatchers.default) {
resultBatches.update { resultBatches ->
val items = resultBatches.uiBatches
val previousList = resultBatches.processedItems
val converter = MarketsTokenItemConverter(
currentTrendInterval = MarketsListUM.TrendInterval.H24,
appCurrency = currentAppCurrency(),
)
if (newList.isEmpty()) {
return@update ResultBatches(processedItems = emptyList())
}
val isInitialLoading =
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
val outItems = if (isInitialLoading) {
newList.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
// As nextBatchSize = 0, we only have one batch, but keep the logic for safety
if (previousList.size != newList.size) {
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
val newBatches = newList.filter { keysToAdd.contains(it.key) }
items + newBatches.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
items.mapIndexed { batchIndex, batch ->
val prevBatch = previousList[batchIndex]
val newBatch = newList[batchIndex]
if (prevBatch == newBatch) return@mapIndexed batch
Batch(
key = batch.key,
data = batch.data.mapIndexed { index, marketsListItemUM ->
val prevItem = prevBatch.data.getOrNull(index)
val newItem = newBatch.data.getOrNull(index)
if (prevItem != null && newItem != null) {
converter.update(prevItem, marketsListItemUM, newItem)
} else {
newItem?.let { converter.convert(it) } ?: marketsListItemUM
}
},
)
}
}
}
currentCoroutineContext().ensureActive()
ResultBatches(
uiBatches = outItems,
processedItems = newList,
)
}
}
fun reload(fiatPriceCurrency: String) {
modelScope.launch(dispatchers.default) {
resultBatches.value = ResultBatches()
stateManager.state.value = BatchListState()
actionsFlow.emit(
BatchAction.Reload(
requestParams = TokenMarketListConfig(
@ -366,16 +314,11 @@ internal class FeedMarketsBatchFlowManager(
}
fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? {
return resultBatches.value.processedItems
return stateManager.state.value.processedItems
?.asSequence()
?.flatMap { it.data }
?.firstOrNull { it.id == tokenId }
}
private data class ResultBatches(
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
)
}
private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM {

View file

@ -1,19 +1,14 @@
package com.tangem.features.feed.ui.feed.state
package com.tangem.features.feed.model.feed.state
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import com.tangem.features.feed.ui.utils.mapFormattedDate
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
import com.tangem.features.feed.ui.feed.state.FeedListUM
import com.tangem.features.feed.ui.feed.state.NewsUM
import com.tangem.features.feed.ui.feed.state.NewsUMState
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
internal class TrendingNewsStateFactory(
private val currentStateProvider: Provider<FeedListUM>,
@ -31,7 +26,9 @@ internal class TrendingNewsStateFactory(
private fun handleDataState(currentState: FeedListUM, articles: List<ShortArticle>) {
val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles)
val commonArticlesUM = commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList()
val commonArticlesUM = getShortArticleConfigConverter(isTrending = false)
.convert(commonArticles)
.toPersistentList()
val updatedNews = when (currentState.news.newsUMState) {
NewsUMState.CONTENT -> currentState.news.copy(content = commonArticlesUM)
NewsUMState.LOADING,
@ -45,7 +42,10 @@ internal class TrendingNewsStateFactory(
onStateUpdate(
currentState.copy(
trendingArticle = trendingArticle?.let { mapToArticleConfigUM(it, isTrending = true) },
trendingArticle = trendingArticle?.let { article ->
getShortArticleConfigConverter(isTrending = true)
.convert(listOf(article))
}?.firstOrNull(),
news = updatedNews,
),
)
@ -77,32 +77,7 @@ internal class TrendingNewsStateFactory(
}
}
private fun mapToArticleConfigUM(article: ShortArticle, isTrending: Boolean): ArticleConfigUM {
return ArticleConfigUM(
id = article.id,
title = article.title,
score = article.score,
isTrending = isTrending,
tags = buildArticleTags(article),
createdAt = mapFormattedDate(article.createdAt),
isViewed = article.viewed,
)
}
private fun buildArticleTags(article: ShortArticle): ImmutableSet<LabelUM> {
val categoryLabels = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}
val tokenLabels = article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
}
return (categoryLabels + tokenLabels).toPersistentSet()
private fun getShortArticleConfigConverter(isTrending: Boolean): ShortArticleToArticleConfigUMConverter {
return ShortArticleToArticleConfigUMConverter(isTrending = Provider { isTrending })
}
}

View file

@ -40,11 +40,11 @@ import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
import com.tangem.features.feed.model.market.details.converter.ExchangeItemStateConverter
import com.tangem.features.feed.model.market.details.converter.RelatedNewsConverter
import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoConverter
import com.tangem.features.feed.model.market.details.formatter.*
import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
import com.tangem.lib.crypto.BlockchainUtils
@ -143,8 +143,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
// ==================
)
private val relatedNewsConverter by lazy {
RelatedNewsConverter()
private val shortArticleToArticleConfigUMConverter by lazy {
ShortArticleToArticleConfigUMConverter(isTrending = Provider { false })
}
private val descriptionConverter = DescriptionConverter(
@ -311,7 +311,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
).onRight { articles ->
state.update { marketsTokenDetailsUM ->
val relatedNews = relatedNewsConverter.convert(articles)
val relatedNews = shortArticleToArticleConfigUMConverter.convert(articles)
marketsTokenDetailsUM.copy(
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
articles = relatedNews,

View file

@ -4,12 +4,15 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.model.converter.BatchItemConverter
import com.tangem.features.feed.model.converter.BatchListStateManager
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
import com.tangem.features.feed.model.converter.distinctBatchesContent
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.model.market.list.utils.logAction
import com.tangem.features.feed.model.market.list.utils.logStatus
import com.tangem.features.feed.model.market.list.utils.logUpdateResults
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchFetchResult
@ -48,8 +51,27 @@ internal class MarketsListBatchFlowManager(
batchFlowType = batchFlowType,
)
private val batchConverter = object : BatchItemConverter<TokenMarket, MarketsListItemUM> {
private val internalConverter: MarketsTokenItemConverter
get() = MarketsTokenItemConverter(
currentTrendInterval = currentTrendInterval(), // Теперь тут всегда актуальное значение
appCurrency = currentAppCurrency(),
)
override fun convert(item: TokenMarket) = internalConverter.convert(item)
override fun update(prevDomain: TokenMarket, currentUI: MarketsListItemUM, newDomain: TokenMarket) =
internalConverter.update(prevDomain, currentUI, newDomain)
}
private val stateManager = BatchListStateManager<Int, TokenMarket, MarketsListItemUM>(
converter = batchConverter,
dispatchers = dispatchers,
)
private val resultBatches = MutableStateFlow(ResultBatches())
private val uiBatches = resultBatches.map { it.uiBatches }
private val uiBatches = stateManager.state.map { it.uiBatches }
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
get() = uiBatches
@ -128,15 +150,11 @@ internal class MarketsListBatchFlowManager(
init {
batchFlow.state
.map { it.data }
.distinctUntilChanged { a, b ->
a.size == b.size &&
a.map { it.key } == b.map { it.key } &&
a.map { it.data }.flatten() == b.map { it.data }.flatten()
}
.onEach {
.distinctBatchesContent()
.onEach { newList ->
coroutineScope {
launch {
updateState(it)
stateManager.update(newList = newList, forceUpdate = false)
}.saveIn(updateStateJob)
}
}
@ -159,69 +177,11 @@ internal class MarketsListBatchFlowManager(
}
}
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
withContext(dispatchers.default) {
resultBatches.update { resultBatches ->
val items = resultBatches.uiBatches
val previousList = resultBatches.processedItems
val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency())
if (newList.isEmpty()) {
return@update ResultBatches(processedItems = emptyList())
}
val isInitialLoading =
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
val outItems = if (isInitialLoading) {
newList.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
if (previousList.size != newList.size) {
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
val newBatches = newList.filter { keysToAdd.contains(it.key) }
items + newBatches.map { batch ->
Batch(
key = batch.key,
data = converter.convertList(batch.data),
)
}
} else {
items.mapIndexed { batchIndex, batch ->
val prevBatch = previousList[batchIndex]
val newBatch = newList[batchIndex]
if (previousList == newBatch) return@mapIndexed batch
Batch(
key = batch.key,
data = batch.data.mapIndexed { index, marketsListItemUM ->
val prevItem = prevBatch.data[index]
val newItem = newBatch.data[index]
converter.update(
prevItem,
marketsListItemUM,
newItem,
)
},
)
}
}
}
currentCoroutineContext().ensureActive()
ResultBatches(
uiBatches = outItems,
processedItems = newList,
)
}
fun updateUIWithSameState() {
modelScope.launch(dispatchers.default) {
val current = batchFlow.state.value.data
stateManager.update(current, forceUpdate = true)
}.saveIn(updateStateJob)
}
fun reload(searchText: String? = null) {
@ -250,13 +210,6 @@ internal class MarketsListBatchFlowManager(
}
}
fun updateUIWithSameState() {
modelScope.launch(dispatchers.default) {
val current = batchFlow.state.value.data
updateState(current, forceUpdate = true)
}.saveIn(updateStateJob)
}
fun loadCharts(batchKeys: Set<Int>, interval: MarketsListUM.TrendInterval) {
if (batchKeys.isEmpty()) return

View file

@ -2,6 +2,7 @@ package com.tangem.features.feed.model.news.details
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -12,16 +13,16 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.RelatedToken
import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter
import com.tangem.features.feed.model.news.details.converter.RelatedTokenConverter
import com.tangem.features.feed.model.news.details.converter.TokenMarketInfoToParamsConverter
import com.tangem.features.feed.model.news.details.factory.NewsDetailsStateFactory
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.news.RelatedToken
import com.tangem.features.feed.ui.news.details.state.ArticleUM
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
@ -49,6 +50,7 @@ internal class NewsDetailsModel @Inject constructor(
private val shareManager: ShareManager,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val markArticleAsViewedUseCase: MarkArticleAsViewedUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -101,7 +103,10 @@ internal class NewsDetailsModel @Inject constructor(
private fun onArticleIndexChanged(newIndex: Int) {
stateFactory.updateSelectedArticleIndex(newIndex)
val currentArticle = state.value.articles.getOrNull(newIndex)
currentArticle?.let(::loadRelatedTokens)
currentArticle?.let { article ->
markArticleAsViewed(article.id)
loadRelatedTokens(article)
}
}
private fun handlePreselectedArticles() {
@ -126,8 +131,6 @@ internal class NewsDetailsModel @Inject constructor(
.onEach { articles ->
val selectedIndex = articles.indexOfFirstOrNull { it.id == params.articleId } ?: 0
stateFactory.updateArticles(articles, selectedIndex)
val currentArticle = articles.getOrNull(selectedIndex)
currentArticle?.let(::loadRelatedTokens)
}
.launchIn(modelScope)
}
@ -255,6 +258,12 @@ internal class NewsDetailsModel @Inject constructor(
}
}
private fun markArticleAsViewed(articleId: Int) {
modelScope.launch(dispatchers.default) {
markArticleAsViewedUseCase.markAsViewed(articleId)
}
}
companion object {
internal const val RELATED_TOKEN_MAX_COUNT = 5
}

View file

@ -1,19 +1,28 @@
package com.tangem.features.feed.model.news.list
import com.tangem.common.ui.R
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.model.news.list.statemanager.NewsListBatchFlowManager
import com.tangem.features.feed.ui.news.list.state.NewsListState
import com.tangem.features.feed.ui.news.list.state.NewsListUM
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.SupportedLanguages
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -22,24 +31,57 @@ import javax.inject.Inject
internal class NewsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase,
private val getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<DefaultNewsListComponent.Params>()
private val selectedCategoryId = MutableStateFlow<Int?>(null)
private val currentLanguage = SupportedLanguages.getCurrentSupportedLanguageCode()
private val batchFlowManager by lazy {
NewsListBatchFlowManager(
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
currentLanguage = Provider { currentLanguage },
currentCategoryIds = Provider {
selectedCategoryId.value?.takeIf { it > 0 }?.let { listOf(it) }.orEmpty()
},
modelScope = modelScope,
dispatchers = dispatchers,
)
}
private val _state = MutableStateFlow(
NewsListUM(
selectedCategoryId = 0,
selectedCategoryId = DEFAULT_ALL_NEWS_CATEGORIES_ID,
filters = persistentListOf(),
articles = persistentListOf(),
onArticleClick = params.onArticleClicked,
newsListState = NewsListState.Loading,
listOfArticles = persistentListOf(),
onArticleClick = { articleId ->
params.onArticleClicked(
/* currentArticle */ articleId,
/* prefetchedArticles */ getCurrentFetchedArticlesIds(),
)
},
onBackClick = params.onBackClick,
),
)
val state = _state.asStateFlow()
init {
loadCategories()
observeNewsList()
batchFlowManager.reload()
}
private fun loadCategories() {
modelScope.launch(dispatchers.default) {
val allCategoriesChip = ChipUM(
id = DEFAULT_ALL_NEWS_CATEGORIES_ID,
text = TextReference.Res(R.string.news_all_news),
isSelected = true,
onClick = { onCategoryClick(DEFAULT_ALL_NEWS_CATEGORIES_ID) },
)
val filterChips = getNewsCategoriesUseCase
.invoke()
.fold(
@ -47,7 +89,7 @@ internal class NewsListModel @Inject constructor(
persistentListOf()
},
ifRight = { categories ->
categories.map { articleCategory ->
(listOf(allCategoriesChip) + categories.map { articleCategory ->
ChipUM(
id = articleCategory.id,
text = TextReference.Str(articleCategory.name),
@ -56,7 +98,7 @@ internal class NewsListModel @Inject constructor(
onCategoryClick(articleCategory.id)
},
)
}.toImmutableList()
}).toPersistentList()
},
)
_state.update { currentState ->
@ -65,18 +107,68 @@ internal class NewsListModel @Inject constructor(
}
}
private fun onCategoryClick(categoryId: Int) {
private fun observeNewsList() {
modelScope.launch(dispatchers.default) {
combine(
batchFlowManager.uiItems,
batchFlowManager.isInInitialLoadingErrorState,
batchFlowManager.paginationStatus,
) { articles, isError, paginationStatus ->
when {
isError -> NewsListState.LoadingError(
onRetryClicked = {
loadCategories()
batchFlowManager.reload()
},
) to persistentListOf()
paginationStatus is PaginationStatus.InitialLoading && articles.isEmpty() -> {
NewsListState.Loading to persistentListOf()
}
articles.isEmpty() -> {
NewsListState.Loading to persistentListOf()
}
else -> {
NewsListState.Content(loadMore = { batchFlowManager.loadMore() }) to articles
}
}
}.collect { (listState, articles) ->
_state.update { currentState ->
currentState.copy(
selectedCategoryId = categoryId,
filters = updateFilterChips(categoryId),
listOfArticles = articles,
newsListState = listState,
)
}
}
}
}
private fun updateFilterChips(categoryId: Int): ImmutableList<ChipUM> {
private fun onCategoryClick(categoryId: Int) {
val newCategoryId = if (state.value.selectedCategoryId == categoryId) {
DEFAULT_ALL_NEWS_CATEGORIES_ID
} else {
categoryId
}
selectedCategoryId.value = newCategoryId
_state.update { currentState ->
currentState.copy(
selectedCategoryId = newCategoryId,
filters = updateFilterChips(newCategoryId),
)
}
batchFlowManager.reload()
}
private fun updateFilterChips(categoryId: Int?): ImmutableList<ChipUM> {
return state.value.filters.map { chip ->
chip.copy(isSelected = chip.id == categoryId)
}.toImmutableList()
}
private fun getCurrentFetchedArticlesIds(): List<Int> {
return state.value.listOfArticles.map { it.id }
}
companion object {
private const val DEFAULT_ALL_NEWS_CATEGORIES_ID = -1
}
}

View file

@ -0,0 +1,122 @@
package com.tangem.features.feed.model.news.list.statemanager
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.news.model.NewsListBatchingContext
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
import com.tangem.features.feed.model.converter.distinctBatchesContent
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class NewsListBatchFlowManager(
getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
private val currentLanguage: Provider<String>,
private val currentCategoryIds: Provider<List<Int>>,
private val modelScope: CoroutineScope,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
private val converter by lazy {
ShortArticleToArticleConfigUMConverter(isTrending = Provider { false })
}
private val batchFlow = getNewsListBatchFlowUseCase(
context = NewsListBatchingContext(
actionsFlow = actionsFlow,
coroutineScope = modelScope,
),
batchSize = DEFAULT_BATCH_SIZE,
)
private val resultBatches = MutableStateFlow<List<Batch<Int, List<ArticleConfigUM>>>>(emptyList())
val uiItems: StateFlow<ImmutableList<ArticleConfigUM>>
get() = batchFlow.state
.map { batchListState ->
batchListState.data
.flatMap { batch -> batch.data }
.let { articles -> converter.convert(articles) }
}
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = persistentListOf(),
)
val isInInitialLoadingErrorState = batchFlow.state
.map { it.status is PaginationStatus.InitialLoadingError }
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
val paginationStatus: StateFlow<PaginationStatus<List<ShortArticle>>>
get() = batchFlow.state
.map { it.status }
.distinctUntilChanged()
.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = PaginationStatus.InitialLoading,
)
init {
batchFlow.state
.map { it.data }
.distinctBatchesContent()
.onEach { batches ->
resultBatches.value = batches.map { batch ->
Batch(
key = batch.key,
data = converter.convert(batch.data),
)
}
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
fun reload() {
modelScope.launch(dispatchers.default) {
resultBatches.value = emptyList()
actionsFlow.emit(
BatchAction.Reload(
requestParams = createNewsListConfig(),
),
)
}
}
fun loadMore() {
modelScope.launch(dispatchers.default) {
actionsFlow.emit(BatchAction.LoadMore())
}
}
private fun createNewsListConfig(): NewsListConfig {
return NewsListConfig(
language = currentLanguage(),
snapshot = null,
tokenIds = emptyList(),
categoryIds = currentCategoryIds(),
)
}
private companion object {
private const val DEFAULT_BATCH_SIZE = 20
}
}

View file

@ -51,12 +51,14 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
pageCount = { state.articles.size },
)
if (state.articles.isNotEmpty()) {
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.collect { page ->
state.onArticleIndexChanged(page)
}
}
}
LaunchedEffect(state.selectedArticleIndex) {
if (pagerState.currentPage != state.selectedArticleIndex) {

View file

@ -1,25 +1,27 @@
package com.tangem.features.feed.ui.news.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.chip.Chip
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn
import com.tangem.features.feed.ui.news.list.state.NewsListState
import com.tangem.features.feed.ui.news.list.state.NewsListUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
@ -27,6 +29,8 @@ import kotlinx.collections.immutable.toImmutableSet
@Composable
internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
val lazyListState = rememberLazyListState()
Column(
modifier = modifier
.fillMaxSize()
@ -43,25 +47,15 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
Chip(state = filter)
}
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
) {
items(
items = state.articles,
key = ArticleConfigUM::id,
) { article ->
ArticleCard(
articleConfigUM = article,
onArticleClick = { state.onArticleClick(article.id) },
modifier = Modifier
.fillMaxWidth()
.height(164.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
SpacerH(16.dp)
NewsListLazyColumn(
newsListState = state.newsListState,
listOfArticles = state.listOfArticles,
lazyListState = lazyListState,
onArticleClick = state.onArticleClick,
)
SpacerH(12.dp)
}
}
}
}
@ -137,7 +131,8 @@ private fun NewsListContentPreview() {
state = NewsListUM(
selectedCategoryId = 0,
filters = filters,
articles = articles,
listOfArticles = articles,
newsListState = NewsListState.Content(loadMore = {}),
onArticleClick = {},
onBackClick = {},
),

View file

@ -0,0 +1,162 @@
package com.tangem.features.feed.ui.news.list.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.common.ui.news.DefaultLoadingArticle
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.news.list.state.NewsListState
import kotlinx.collections.immutable.ImmutableList
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 40
@Composable
internal fun NewsListLazyColumn(
listOfArticles: ImmutableList<ArticleConfigUM>,
newsListState: NewsListState,
lazyListState: LazyListState,
onArticleClick: (Int) -> Unit,
) {
val screenState by remember(listOfArticles, newsListState) {
derivedStateOf {
val isListEmpty = listOfArticles.isEmpty()
when {
isListEmpty && newsListState is NewsListState.Loading -> NewsListScreenState.InitialLoading
isListEmpty && newsListState is NewsListState.LoadingError -> NewsListScreenState.InitialError
else -> NewsListScreenState.Content
}
}
}
AnimatedContent(targetState = screenState, label = "NewsListTransition") { state ->
when (state) {
NewsListScreenState.Content -> {
Content(
listOfArticles = listOfArticles,
newsListState = newsListState,
lazyListState = lazyListState,
onArticleClick = onArticleClick,
)
}
NewsListScreenState.InitialLoading -> {
LazyColumn(
state = rememberLazyListState(),
contentPadding = PaddingValues(bottom = 16.dp, start = 16.dp, end = 16.dp),
userScrollEnabled = false,
) {
items(
count = 10,
key = { "initial_loading_$it" },
) {
DefaultLoadingArticle(
modifier = Modifier
.fillMaxWidth()
.height(164.dp),
)
SpacerH(12.dp)
}
}
}
NewsListScreenState.InitialError -> {
val errorState = newsListState as? NewsListState.LoadingError
LoadingErrorItem(
modifier = Modifier.fillMaxSize(),
onTryAgain = errorState?.onRetryClicked ?: {},
)
}
}
}
}
@Composable
private fun Content(
listOfArticles: ImmutableList<ArticleConfigUM>,
newsListState: NewsListState,
lazyListState: LazyListState,
onArticleClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
state = lazyListState,
contentPadding = PaddingValues(bottom = 16.dp, start = 16.dp, end = 16.dp),
userScrollEnabled = true,
) {
items(
items = listOfArticles,
key = ArticleConfigUM::id,
) { article ->
ArticleCard(
modifier = Modifier
.fillMaxWidth()
.height(164.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
articleConfigUM = article,
onArticleClick = {
onArticleClick(article.id)
},
)
SpacerH(12.dp)
}
if (newsListState is NewsListState.Loading) {
items(
count = 10,
key = { "loading_footer_$it" },
) {
DefaultLoadingArticle(
modifier = Modifier
.fillMaxWidth()
.height(164.dp),
)
SpacerH(12.dp)
}
}
}
if (newsListState is NewsListState.Content) {
InfiniteListHandler(
listState = lazyListState,
buffer = LOAD_NEXT_PAGE_ON_END_INDEX,
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(newsListState) {
{
newsListState.loadMore()
true
}
},
)
}
}
private enum class NewsListScreenState {
InitialLoading,
InitialError,
Content,
}
@Composable
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
Box(
modifier = modifier.padding(vertical = 35.dp, horizontal = 10.dp),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = onTryAgain)
}
}

View file

@ -7,9 +7,17 @@ import kotlinx.collections.immutable.ImmutableList
@Immutable
data class NewsListUM(
val selectedCategoryId: Int?,
val selectedCategoryId: Int,
val filters: ImmutableList<ChipUM>,
val articles: ImmutableList<ArticleConfigUM>,
val listOfArticles: ImmutableList<ArticleConfigUM>,
val newsListState: NewsListState,
val onArticleClick: (Int) -> Unit,
val onBackClick: () -> Unit,
)
@Immutable
sealed class NewsListState {
data class Content(val loadMore: () -> Unit) : NewsListState()
data object Loading : NewsListState()
data class LoadingError(val onRetryClicked: () -> Unit) : NewsListState()
}