feat: add pagination support and some refactorings
This commit is contained in:
parent
dd2810f516
commit
a1382d0ee4
9 changed files with 112 additions and 18 deletions
|
@ -10,7 +10,7 @@ import xyz.leomurca.csgomatches.data.source.MatchDataSource
|
|||
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||
|
||||
class MatchLocalDataSource : MatchDataSource {
|
||||
override suspend fun upcomingMatches(): Resource<List<MatchDto>> {
|
||||
override suspend fun upcomingMatches(page: Int): Resource<List<MatchDto>> {
|
||||
return Resource.Success(
|
||||
data = listOf(
|
||||
// Happy path
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package xyz.leomurca.csgomatches.data.remote
|
||||
|
||||
import androidx.compose.foundation.pager.PageSize
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.leomurca.csgomatches.data.model.ErrorDto
|
||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||
|
@ -11,9 +12,9 @@ class MatchRemoteDataSourceImpl(
|
|||
private val matchesApiService: MatchesApiService,
|
||||
private val json: Json
|
||||
) : MatchDataSource {
|
||||
override suspend fun upcomingMatches(): Resource<List<MatchDto>> {
|
||||
override suspend fun upcomingMatches(page: Int): Resource<List<MatchDto>> {
|
||||
return try {
|
||||
val response = matchesApiService.upcomingMatches()
|
||||
val response = matchesApiService.upcomingMatches(page = page)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
Resource.Success(response.body() ?: throw Exception("Empty response body"))
|
||||
|
|
|
@ -5,14 +5,20 @@ import retrofit2.http.GET
|
|||
import retrofit2.http.Query
|
||||
import xyz.leomurca.csgomatches.data.model.MatchDto
|
||||
import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
||||
import xyz.leomurca.csgomatches.utils.ApiDefaults.ITEMS_PER_PAGE
|
||||
import xyz.leomurca.csgomatches.utils.ApiDefaults.MATCH_FILTER_STATUS
|
||||
import xyz.leomurca.csgomatches.utils.ApiDefaults.MATCH_FINISHED
|
||||
import xyz.leomurca.csgomatches.utils.ApiDefaults.MATCH_SORT
|
||||
|
||||
interface MatchesApiService {
|
||||
|
||||
@GET("matches")
|
||||
suspend fun upcomingMatches(
|
||||
@Query("filter[status]") filterStatus: String = "running, not_started",
|
||||
@Query("finished") finished: Boolean = false,
|
||||
@Query("sort") sort: String = "begin_at",
|
||||
@Query("filter[status]") filterStatus: String = MATCH_FILTER_STATUS,
|
||||
@Query("finished") finished: Boolean = MATCH_FINISHED,
|
||||
@Query("sort") sort: String = MATCH_SORT,
|
||||
@Query("per_page") perPage: Int = ITEMS_PER_PAGE,
|
||||
@Query("page") page: Int,
|
||||
): Response<List<MatchDto>>
|
||||
|
||||
@GET("teams")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package xyz.leomurca.csgomatches.data.repository
|
||||
|
||||
import androidx.compose.foundation.pager.PageSize
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import xyz.leomurca.csgomatches.data.mapper.toDomain
|
||||
|
@ -15,9 +16,9 @@ class MatchRepositoryImpl(
|
|||
@Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
private val matchRemoteDataSource: MatchDataSource
|
||||
) : MatchRepository {
|
||||
override suspend fun upcomingMatches(): Resource<List<Match>> {
|
||||
override suspend fun upcomingMatches(page: Int): Resource<List<Match>> {
|
||||
return withContext(ioDispatcher) {
|
||||
when (val result = matchRemoteDataSource.upcomingMatches()) {
|
||||
when (val result = matchRemoteDataSource.upcomingMatches(page)) {
|
||||
is Resource.Success -> Resource.Success(result.data.map { it.toDomain() })
|
||||
is Resource.Error -> Resource.Error(result.message)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,6 @@ import xyz.leomurca.csgomatches.data.model.TeamDetailsDto
|
|||
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||
|
||||
interface MatchDataSource {
|
||||
suspend fun upcomingMatches(): Resource<List<MatchDto>>
|
||||
suspend fun upcomingMatches(page: Int): Resource<List<MatchDto>>
|
||||
suspend fun teamDetails(teamId: String): Resource<List<TeamDetailsDto>>
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
package xyz.leomurca.csgomatches.domain.repository
|
||||
|
||||
import androidx.compose.foundation.pager.PageSize
|
||||
import xyz.leomurca.csgomatches.domain.model.Match
|
||||
import xyz.leomurca.csgomatches.domain.model.Resource
|
||||
import xyz.leomurca.csgomatches.domain.model.Team
|
||||
|
||||
interface MatchRepository {
|
||||
|
||||
suspend fun upcomingMatches(): Resource<List<Match>>
|
||||
suspend fun upcomingMatches(page: Int): Resource<List<Match>>
|
||||
|
||||
suspend fun teamDetails(teamId: String): Resource<List<Team>>
|
||||
}
|
|
@ -2,11 +2,16 @@ package xyz.leomurca.csgomatches.ui.screens.matches
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
@ -15,7 +20,10 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -35,6 +43,17 @@ fun MatchesScreen(
|
|||
) {
|
||||
val uiState = viewModel.uiState.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||
.collect { lastVisibleIndex ->
|
||||
val totalItems = listState.layoutInfo.totalItemsCount
|
||||
if (lastVisibleIndex != null && lastVisibleIndex >= totalItems - 3) {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
|
@ -65,7 +84,13 @@ fun MatchesScreen(
|
|||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
when (val value = uiState.value) {
|
||||
is MatchesUiState.Success -> MatchesList(value.matches, onTapCard)
|
||||
is MatchesUiState.Success -> MatchesList(
|
||||
matches = value.matches,
|
||||
onTapCard = onTapCard,
|
||||
listState = listState,
|
||||
isPaginating = value.isPaginating
|
||||
)
|
||||
|
||||
is MatchesUiState.Error -> ErrorMessage(
|
||||
message = value.message,
|
||||
onRetry = { viewModel.loadUpcomingMatches() }
|
||||
|
@ -78,15 +103,34 @@ fun MatchesScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MatchesList(matches: List<Match>, onTapCard: (MatchDetailsRoute) -> Unit) {
|
||||
fun MatchesList(
|
||||
matches: List<Match>,
|
||||
onTapCard: (MatchDetailsRoute) -> Unit,
|
||||
listState: LazyListState,
|
||||
isPaginating: Boolean
|
||||
) {
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
contentPadding = PaddingValues(vertical = 24.dp)
|
||||
contentPadding = PaddingValues(vertical = 24.dp),
|
||||
state = listState,
|
||||
) {
|
||||
items(matches) {
|
||||
MatchCard(it, onTapCard)
|
||||
items(matches) { match ->
|
||||
MatchCard(match, onTapCard)
|
||||
}
|
||||
|
||||
if (isPaginating) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,23 +20,56 @@ class MatchesViewModel @Inject constructor(
|
|||
private val _uiState = MutableStateFlow<MatchesUiState>(MatchesUiState.Loading)
|
||||
val uiState: StateFlow<MatchesUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var currentPage = 1
|
||||
private var isLoadingNextPage = false
|
||||
|
||||
init {
|
||||
loadUpcomingMatches()
|
||||
}
|
||||
|
||||
fun loadUpcomingMatches() {
|
||||
currentPage = 1
|
||||
viewModelScope.launch {
|
||||
_uiState.value = MatchesUiState.Loading
|
||||
_uiState.value = when (val result = matchRepository.upcomingMatches()) {
|
||||
_uiState.value = when (val result = matchRepository.upcomingMatches(currentPage)) {
|
||||
is Resource.Success -> MatchesUiState.Success(result.data)
|
||||
is Resource.Error -> MatchesUiState.Error(result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
val current = _uiState.value as? MatchesUiState.Success ?: return
|
||||
if (isLoadingNextPage || current.endReached) return
|
||||
|
||||
isLoadingNextPage = true
|
||||
currentPage++
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = current.copy(isPaginating = true)
|
||||
|
||||
val result = matchRepository.upcomingMatches(currentPage)
|
||||
val newMatches = (result as? Resource.Success)?.data.orEmpty()
|
||||
|
||||
_uiState.value = current.copy(
|
||||
matches = current.matches + newMatches,
|
||||
isPaginating = false,
|
||||
endReached = newMatches.size < 20
|
||||
)
|
||||
|
||||
isLoadingNextPage = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed class MatchesUiState {
|
||||
object Loading : MatchesUiState()
|
||||
data class Success(val matches: List<Match>) : MatchesUiState()
|
||||
data class Success(
|
||||
val matches: List<Match>,
|
||||
val isPaginating: Boolean = false,
|
||||
val endReached: Boolean = false
|
||||
) : MatchesUiState()
|
||||
|
||||
data class Error(val message: String) : MatchesUiState()
|
||||
object Loading : MatchesUiState()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package xyz.leomurca.csgomatches.utils
|
||||
|
||||
object ApiDefaults {
|
||||
const val ITEMS_PER_PAGE = 20
|
||||
const val MATCH_FILTER_STATUS = "running, not_started"
|
||||
const val MATCH_FINISHED = false
|
||||
const val MATCH_SORT = "begin_at"
|
||||
}
|
Loading…
Add table
Reference in a new issue