Updated on 2026-08-14
This commit is contained in:
parent
9105ffbd75
commit
dcb8b474cf
20 changed files with 550 additions and 8 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue