Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-18 08:47:52 +01:00
parent 9105ffbd75
commit dcb8b474cf
20 changed files with 550 additions and 8 deletions

View file

@ -1,7 +1,9 @@
package com.tangem.data.news.di
import com.tangem.data.news.repository.DefaultNewsRepository
import com.tangem.datasource.api.news.NewsApi
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -14,7 +16,10 @@ internal object NewsDataModule {
@Singleton
@Provides
fun providesQuotesRepository(): NewsRepository {
return DefaultNewsRepository()
fun provideNewsRepository(newsApi: NewsApi, dispatchers: CoroutineDispatcherProvider): NewsRepository {
return DefaultNewsRepository(
newsApi = newsApi,
dispatchers = dispatchers,
)
}
}

View file

@ -1,10 +1,170 @@
package com.tangem.data.news.repository
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.news.NewsApi
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.models.ArticleCategory
import com.tangem.domain.news.models.DetailedArticle
import com.tangem.domain.news.models.ShortArticle
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.BatchListSource
import com.tangem.pagination.exception.EndOfPaginationException
import com.tangem.pagination.fetcher.BatchFetcher
import com.tangem.pagination.toBatchFlow
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
/**
* Default implementation of [NewsRepository]
*
* Implementation of [NewsRepository].
[REDACTED_AUTHOR]
*/
internal class DefaultNewsRepository : NewsRepository
internal class DefaultNewsRepository @Inject constructor(
private val newsApi: NewsApi,
private val dispatchers: CoroutineDispatcherProvider,
) : NewsRepository {
override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow {
return BatchListSource(
fetchDispatcher = dispatchers.io,
context = context,
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: INITIAL_BATCH_KEY },
batchFetcher = createBatchFetcher(batchSize),
).toBatchFlow()
}
override suspend fun getDetailedArticle(newsId: Int, language: String?): DetailedArticle {
val response = newsApi.getNewsDetails(newsId = newsId, language = language).getOrThrow()
return response.toDomainDetailedArticle()
}
override suspend fun getTrendingNews(limit: Int, language: String?): List<ShortArticle> {
val response = newsApi.getTrendingNews(limit = limit, language = language).getOrThrow()
return response.items.map { it.toDomainShortArticle() }
}
override suspend fun getCategories(): List<ArticleCategory> {
return newsApi.getCategories().getOrThrow().items.map { dto ->
ArticleCategory(
id = dto.id,
name = dto.name,
)
}
}
private fun createBatchFetcher(batchSize: Int): BatchFetcher<NewsListConfig, List<ShortArticle>> {
return NewsBatchFetcher(
newsApi = newsApi,
batchSize = batchSize,
)
}
private class NewsBatchFetcher(
private val newsApi: NewsApi,
private val batchSize: Int,
) : BatchFetcher<NewsListConfig, List<ShortArticle>> {
private var state: NewsPaginationState? = null
override suspend fun fetchFirst(requestParams: NewsListConfig): BatchFetchResult<List<ShortArticle>> {
return runCatching {
loadPage(
page = FIRST_PAGE,
params = requestParams,
limit = batchSize,
snapshotOverride = requestParams.snapshot,
)
}.fold(
onSuccess = { result ->
state = result.state
result.batchResult
},
onFailure = { throwable -> BatchFetchResult.Error(throwable) },
)
}
override suspend fun fetchNext(
overrideRequestParams: NewsListConfig?,
lastResult: BatchFetchResult<List<ShortArticle>>,
): BatchFetchResult<List<ShortArticle>> {
val currentState = state ?: return BatchFetchResult.Error(
IllegalStateException("fetchFirst must be called"),
)
if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val params = overrideRequestParams ?: currentState.params
val shouldReset = overrideRequestParams != null && overrideRequestParams != currentState.params
val pageToLoad = if (shouldReset) FIRST_PAGE else currentState.nextPage
val snapshot = if (shouldReset) overrideRequestParams.snapshot else currentState.snapshot
return runCatching {
loadPage(
page = pageToLoad,
params = params,
limit = batchSize,
snapshotOverride = snapshot,
)
}.fold(
onSuccess = { result ->
state = result.state
result.batchResult
},
onFailure = { throwable -> BatchFetchResult.Error(throwable) },
)
}
private suspend fun loadPage(
page: Int,
params: NewsListConfig,
limit: Int,
snapshotOverride: String?,
): PageLoadResult {
val response = newsApi.getNews(
page = page,
limit = limit,
language = params.language,
snapshot = snapshotOverride,
tokenIds = params.tokenIds.takeIf { it.isNotEmpty() },
categoryIds = params.categoryIds.takeIf { it.isNotEmpty() },
).getOrThrow()
val items = response.items.map { it.toDomainShortArticle() }
val batchResult = BatchFetchResult.Success(
data = items,
empty = items.isEmpty(),
last = response.meta.hasNext.not(),
)
return PageLoadResult(
batchResult = batchResult,
state = NewsPaginationState(
nextPage = response.meta.page + 1,
snapshot = response.meta.asOf,
params = params.copy(snapshot = response.meta.asOf),
),
)
}
}
private data class PageLoadResult(
val batchResult: BatchFetchResult.Success<List<ShortArticle>>,
val state: NewsPaginationState,
)
private data class NewsPaginationState(
val nextPage: Int,
val snapshot: String?,
val params: NewsListConfig,
)
private companion object {
private const val INITIAL_BATCH_KEY = 0
private const val FIRST_PAGE = 1
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.data.news.repository
import com.tangem.datasource.api.news.models.response.NewsArticleDto
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
import com.tangem.datasource.api.news.models.response.NewsOriginalArticleDto
import com.tangem.datasource.api.news.models.response.NewsRelatedTokenDto
import com.tangem.domain.news.models.*
internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle {
return DetailedArticle(
id = id,
createdAt = createdAt,
score = score.toFloat(),
locale = language,
isTrending = isTrending,
categories = categories.map { ArticleCategory(id = it.id, name = it.name) },
relatedTokens = relatedTokens.map { it.toDomainRelatedToken() },
title = title,
newsUrl = newsUrl,
shortContent = shortContent,
content = content,
originalArticles = originalArticles.map { it.toDomainOriginalArticle() },
)
}
internal fun NewsArticleDto.toDomainShortArticle(): ShortArticle {
return ShortArticle(
id = id,
createdAt = createdAt,
score = score.toFloat(),
locale = language,
categories = categories.map { ArticleCategory(id = it.id, name = it.name) },
relatedTokens = relatedTokens.map { it.toDomainRelatedToken() },
isTrending = isTrending,
title = title,
newsUrl = newsUrl,
)
}
internal fun NewsRelatedTokenDto.toDomainRelatedToken(): RelatedToken {
return RelatedToken(
id = id,
symbol = symbol,
name = name,
)
}
internal fun NewsOriginalArticleDto.toDomainOriginalArticle(): OriginalArticle {
return OriginalArticle(
id = id,
title = title,
sourceName = sourceName,
locale = language,
publishedAt = publishedAt,
url = url,
imageUrl = imageUrl,
)
}