Ubisam-MobileGit/tasks/034-favorite-star-count-not-incrementing.md
choiminyeong 2e8c9e7987 TASK-034 follow-up: repeat tap on a starred row now un-stars instead of no-op
The prior fix for double-counting used an early-return guard that made
a repeat tap on an already-Starred row do nothing -- fixed the count
bug but also made the star impossible to undo from the ALL list.
Replaced it with a real toggle: an already-starred-this-session tap
now calls unstar() and decrements the count back down instead of being
inert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 16:42:17 +09:00

6.4 KiB

TASK-034: Star count doesn't increment, then repeat tap doesn't un-star

Status: implemented; all required checks pass; live-installed, awaiting the human product owner's confirmation Owner: Claude Reviewer: Codex

Outcome

Two rounds of live feedback on the same feature, following TASK-033's OAuth scope fix. First: the human product owner confirmed starring itself now works ("별표 눌러서 즐겨찾기는 되는데") but the displayed star count didn't go up ("카운팅은 안올라가네") — toggleFavorite's ALL-list success branch updated only favoriteAction (to Starred) and never touched starsCount. Fixed by incrementing starsCount in the same state update. Second, immediately after: with a naive repeat-tap guard in place, tapping an already-starred row no longer un-starred it either ("이제 올라가는데 다시 눌렀을때 취소가 안되네") — the first fix's guard made a repeat tap a dead no-op instead of a real toggle. Replaced that guard with an actual toggle: a repeat tap on a row this session knows is starred now calls unstar() and decrements the count back down.

Scope

  • Included: feature/repositories/RepositoryListViewModel.kt (toggleFavorite only), matching test additions in RepositoryListViewModelTest.kt.
  • Excluded: No change to the FAVORITES-list unstar path (removing the row entirely already made its count irrelevant, no bug there). No change to how the initial list load populates starsCount (that already came from Gitea's own response, unaffected). No attempt to fetch the server's authoritative post-star/unstar count instead of adjusting it client-side (see Design notes).

Design notes

  • Optimistic ±1, not a re-fetch: Gitea's star/unstar endpoints (PUT/DELETE, 204 No Content) don't return the new count, so getting the authoritative number back would mean a second network call purely to update one number — not worth it for a value the app already knows how to derive from something it just confirmed itself (the action succeeded, so the count is exactly one higher or lower than what was last displayed). starsCount is floored at 0 via coerceAtLeast(0) as a defensive guard, not because normal use can reach it.
  • A real toggle, not a guard: the first pass at this fix added an early-return guard that made a repeat tap on an already-Starred row do nothing — that stopped the double-count bug but also, as immediately reported, made the star impossible to undo from the ALL list without switching to the FAVORITES tab. The actual fix branches on the row's own favoriteAction: not-yet-starred-this- session taps call star() (+1, → Starred), already-starred-this-session taps call unstar() (-1, → Idle) — this both fixes the double-count and restores the obviously-expected behavior of tapping a filled star to unfill it.
  • The underlying "can't tell if already starred on load" limitation is untouched: this toggle only tracks what happened within this session's own interaction with a row, same as favoriteAction always has (TASK-018/TASK-030). A repository already starred before the app session started still shows outlined/untapped until acted on directly.

Acceptance criteria

  • Starring a repository from the ALL list increments its displayed star count by exactly 1.
  • Tapping an already-Starred row again un-stars it: calls unstar(), decrements the count by 1, and returns the row to Idle (outlined, tappable to star again).
  • The count never displays below 0.
  • Unstarring from the FAVORITES list is unaffected (row removal, no count to update).
  • Build, unit tests, and lint pass.
  • Live confirmation from the human product owner that both starring (count up) and un-starring (count back down) now work as expected from the ALL list — installed on the device but not yet re-tested in this exact session.

Required checks

./gradlew compileDebugKotlin compileDebugUnitTestKotlin
./gradlew testDebugUnitTest
./gradlew assembleDebug lintDebug

Evidence

  • RepositoryListViewModel.kt: toggleFavorite now branches on whether the row is already Starred this session — un-star path (unstar(), starsCount - 1 coerced at 0, → Idle) and star path (star(), starsCount + 1, → Starred) are both explicit when branches, replacing the first pass's early-return no-op guard.
  • RepositoryListViewModelTest.kt: repo() test helper gained a starsCount parameter; tests cover starring incrementing the count by 1, a repeat tap un-starring (calls both star() and unstar(), decrements the count, returns to Idle), and a full star→unstar round trip landing back at the original count.
  • ./gradlew compileDebugKotlin compileDebugUnitTestKotlin: BUILD SUCCESSFUL.
  • ./gradlew testDebugUnitTest: BUILD SUCCESSFUL, all unit tests passing.
  • ./gradlew assembleDebug lintDebug: BUILD SUCCESSFUL, 0 lint errors.
  • Installed on the human product owner's device over wireless adb (adb install -r, streamed install succeeded) twice across this task's two rounds; not yet re-tested live against the final toggle behavior in this exact session.

Known limitations

  • Still optimistic, not authoritative: if Gitea's real count differs from this app's local ±1 for any reason (e.g. another client starred/unstarred the same repo around the same time), the displayed number stays off until the next full list reload. Considered an acceptable tradeoff given the alternative (a second network call per action) for a rarely-contended personal count.
  • The underlying "can't tell if already starred on initial load" limitation (TASK-018/TASK-030) is unchanged — a repository starred before the current app session still shows outlined until acted on directly in this session, same as before this task.
  • A failed unstar attempt (network error mid-toggle) still collapses to the same flat Failed state as a failed star attempt, which renders as outlined/not-starred on the ALL list even though the repository may still actually be starred server-side — FavoriteActionUiState.Failed carries no memory of which action was being attempted. Accepted as a minor, retryable rendering gap rather than adding state to track it, since a retry from that visual state is harmless either way (an unwanted second star() on an already-starred repo is a no-op server-side).

Review findings

None yet — pending Codex/human review.