fix: leaderboard empty on first open — decouple from loadAll()

LeaderboardScreen called loadAll() which uses Future.wait across 4 APIs.
If getDashboard() fails (empty user_id before auth), the entire batch
throws and leaderboard stays []. Switching districts worked because
setDistrict() calls getLeaderboard() directly.

- Add loadLeaderboard() to GamificationProvider — calls only getLeaderboard(),
  independent of dashboard/shop/achievements
- Add isLeaderboardLoading field with correct lifecycle in setDistrict/setTimePeriod
- LeaderboardScreen.initState now calls loadLeaderboard() instead of loadAll()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:13:56 +05:30
parent 7bc396bdde
commit c7e66756f9
2 changed files with 685 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ class GamificationProvider extends ChangeNotifier {
String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month'
bool isLoading = false;
bool isLeaderboardLoading = false;
String? error;
// TTL guard — prevents redundant API calls from multiple screens
@@ -80,12 +81,36 @@ class GamificationProvider extends ChangeNotifier {
}
}
// ---------------------------------------------------------------------------
// Load only leaderboard data — safe to call independently of loadAll().
// Used by LeaderboardScreen so a dashboard/shop failure doesn't blank the list.
// ---------------------------------------------------------------------------
Future<void> loadLeaderboard() async {
if (isLeaderboardLoading) return;
isLeaderboardLoading = true;
notifyListeners();
try {
final response = await _service.getLeaderboard(
district: leaderboardDistrict,
timePeriod: leaderboardTimePeriod,
);
leaderboard = response.entries;
currentUserStats = response.currentUser;
totalParticipants = response.totalParticipants;
} catch (e) {
error = userFriendlyError(e);
}
isLeaderboardLoading = false;
notifyListeners();
}
// ---------------------------------------------------------------------------
// Change district filter
// ---------------------------------------------------------------------------
Future<void> setDistrict(String district) async {
if (leaderboardDistrict == district) return;
leaderboardDistrict = district;
isLeaderboardLoading = true;
notifyListeners();
try {
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
@@ -95,6 +120,7 @@ class GamificationProvider extends ChangeNotifier {
} catch (e) {
error = userFriendlyError(e);
}
isLeaderboardLoading = false;
notifyListeners();
}
@@ -104,6 +130,7 @@ class GamificationProvider extends ChangeNotifier {
Future<void> setTimePeriod(String period) async {
if (leaderboardTimePeriod == period) return;
leaderboardTimePeriod = period;
isLeaderboardLoading = true;
notifyListeners();
try {
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
@@ -113,6 +140,7 @@ class GamificationProvider extends ChangeNotifier {
} catch (e) {
error = userFriendlyError(e);
}
isLeaderboardLoading = false;
notifyListeners();
}