fix(characters:presentation): pagination race + silent restore failure (review)

- Race: isLoadingNextPage was set inside the launched coroutine, so a rapid second
  OnLoadNextPage passed the guard before the flag flipped -> the same page loaded twice and
  items were appended twice. Set the loading flag synchronously before launching.
- Restore: when a middle page failed after earlier pages loaded, the error was swallowed
  (error=null, no event). Now any restore failure emits a ShowSnackbar; partial restores show
  the loaded list + snackbar, full failures show the error state.

Found by the milestone review.
This commit is contained in:
2026-06-10 13:03:09 +02:00
parent ef50094e3e
commit 38d8f5915b

View File

@@ -52,9 +52,9 @@ class CharacterListViewModel(
}
private fun restore(targetPage: Int) {
// Flip the flag synchronously so a guard reading state sees it immediately.
_state.update { it.copy(isLoading = true, error = null) }
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
val accumulated = mutableListOf<CharacterUi>()
var lastLoadedPage = 0
var endReached = false
@@ -77,9 +77,14 @@ class CharacterListViewModel(
page++
}
if (accumulated.isEmpty() && error != null) {
_state.update { it.copy(isLoading = false, error = error) }
// Always surface a failure — even a partial one where earlier pages loaded.
if (error != null) {
_events.send(CharacterListEvent.ShowSnackbar(error))
}
if (accumulated.isEmpty()) {
// Nothing loaded → full-screen error (or an empty list if the API simply had none).
_state.update { it.copy(isLoading = false, error = error) }
} else {
val loadedPage = lastLoadedPage.coerceAtLeast(1)
_state.update {
@@ -88,6 +93,7 @@ class CharacterListViewModel(
isLoading = false,
currentPage = loadedPage,
endReached = endReached,
// The list is shown; the snackbar already surfaced any partial failure.
error = null,
)
}
@@ -112,8 +118,11 @@ class CharacterListViewModel(
}
private fun loadPage(page: Int) {
// Flip the loading flag SYNCHRONOUSLY (before launching) so a rapid second OnLoadNextPage is
// guarded out before its coroutine starts — otherwise the same page loads twice and items
// get appended twice.
_state.update { it.copy(isLoadingNextPage = true, error = null) }
viewModelScope.launch {
_state.update { it.copy(isLoadingNextPage = true, error = null) }
characterRepository.getCharacters(page)
.onSuccess { pageData ->
_state.update { state ->