Skip to content

added CharacterListViewModel and states with tests#4

Merged
UllashPodder merged 1 commit into
mainfrom
feat/phase-3-viewmodel
Jul 21, 2026
Merged

added CharacterListViewModel and states with tests#4
UllashPodder merged 1 commit into
mainfrom
feat/phase-3-viewmodel

Conversation

@UllashPodder

@UllashPodder UllashPodder commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Title: feat: add CharacterListViewModel with pagination and dedupe guard

What this adds

CharacterListViewModel — the state and pagination logic behind the character list, built test-first with no UI attached yet.

  • CharacterListState: idle / loading / loaded / error(String)
  • loadNextPageIfNeeded(): fetches the next page from CharacterRepository, appends results, advances the page counter, and stops once info.pages is reached
  • A re-entrancy guard (isLoading) that prevents duplicate fetches when the method is called multiple times before the first call completes

Why the guard matters

await releases the actor it's isolated to. When loadNextPageIfNeeded() suspends at the network call, the @mainactor is free, and if the method is called again before that first call resumes, it walks straight through and starts a second fetch for the same page.

This isn't a hypothetical. In a List, onAppear can fire multiple times in quick succession as rows near the bottom during a fast scroll, well before a slow network response returns. Without a guard, that produces duplicate API calls and duplicate rows appended to the list.

The guard closes the gap: the first call sets isLoading = true before it suspends, and any call that arrives while it's still in flight returns immediately instead of starting another fetch.

How it's proven, not just asserted

concurrentLoadsRequestPageOnlyOnce launches three concurrent calls via withTaskGroup, with an artificial delay in the mock repository to force them to genuinely overlap rather than run sequentially. It asserts the repository received the request exactly once.

Confirmed the test is real by temporarily removing the guard: the test failed with requestedPages == [1, 1, 1] and characters.count == 60, then passed again once the guard was restored.

Tests

  • loadsFirstPageAndPopulatesCharacters
  • loadsSecondPageAndAppends
  • concurrentLoadsRequestPageOnlyOnce
  • failedLoadSetsErrorStateAndKeepsCharactersEmpty

@UllashPodder
UllashPodder merged commit 04bff5a into main Jul 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant