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

- Add isLeaderboardLoading flag separate from isLoading
- Add loadLeaderboard() method that fires independently of loadAll TTL
- Remove leaderboard from loadAll() Future.wait (failures in dashboard/shop
  no longer silently zero-out leaderboard data)
- setDistrict / setTimePeriod now use isLeaderboardLoading
- contribute_screen calls loadLeaderboard() alongside loadAll() on mount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:35:51 +05:30
parent 7bc396bdde
commit 4c57391bbd
2 changed files with 36 additions and 11 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
@@ -44,7 +45,6 @@ class GamificationProvider extends ChangeNotifier {
try {
final results = await Future.wait([
_service.getDashboard(),
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod),
_service.getShopItems(),
_service.getAchievements(),
]);
@@ -53,16 +53,11 @@ class GamificationProvider extends ChangeNotifier {
profile = dashboard.profile;
submissions = dashboard.submissions;
final lbResponse = results[1] as LeaderboardResponse;
leaderboard = lbResponse.entries;
currentUserStats = lbResponse.currentUser;
totalParticipants = lbResponse.totalParticipants;
shopItems = results[2] as List<ShopItem>;
shopItems = results[1] as List<ShopItem>;
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
final dashAchievements = dashboard.achievements;
final fetchedAchievements = results[3] as List<AchievementBadge>;
final fetchedAchievements = results[2] as List<AchievementBadge>;
if (dashAchievements.isNotEmpty) {
achievements = dashAchievements;
@@ -80,12 +75,35 @@ class GamificationProvider extends ChangeNotifier {
}
}
// ---------------------------------------------------------------------------
// Load leaderboard independently (decoupled from loadAll)
// ---------------------------------------------------------------------------
Future<void> loadLeaderboard() async {
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);
} finally {
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);
@@ -94,6 +112,8 @@ class GamificationProvider extends ChangeNotifier {
totalParticipants = response.totalParticipants;
} catch (e) {
error = userFriendlyError(e);
} finally {
isLeaderboardLoading = false;
}
notifyListeners();
}
@@ -104,6 +124,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);
@@ -112,6 +133,8 @@ class GamificationProvider extends ChangeNotifier {
totalParticipants = response.totalParticipants;
} catch (e) {
error = userFriendlyError(e);
} finally {
isLeaderboardLoading = false;
}
notifyListeners();
}

View File

@@ -104,7 +104,9 @@ class _ContributeScreenState extends State<ContributeScreen>
super.initState();
PostHogService.instance.screen('Contribute');
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<GamificationProvider>().loadAll();
final p = context.read<GamificationProvider>();
p.loadAll();
p.loadLeaderboard(); // independent — always fires regardless of loadAll TTL
});
}
@@ -274,7 +276,7 @@ class _ContributeScreenState extends State<ContributeScreen>
const SizedBox(height: 16),
// Leaderboard List
if (provider.isLoading && leaderboard.isEmpty)
if (provider.isLeaderboardLoading && leaderboard.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 60),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),