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' String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month'
bool isLoading = false; bool isLoading = false;
bool isLeaderboardLoading = false;
String? error; String? error;
// TTL guard — prevents redundant API calls from multiple screens // TTL guard — prevents redundant API calls from multiple screens
@@ -44,7 +45,6 @@ class GamificationProvider extends ChangeNotifier {
try { try {
final results = await Future.wait([ final results = await Future.wait([
_service.getDashboard(), _service.getDashboard(),
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod),
_service.getShopItems(), _service.getShopItems(),
_service.getAchievements(), _service.getAchievements(),
]); ]);
@@ -53,17 +53,12 @@ class GamificationProvider extends ChangeNotifier {
profile = dashboard.profile; profile = dashboard.profile;
submissions = dashboard.submissions; submissions = dashboard.submissions;
final lbResponse = results[1] as LeaderboardResponse; shopItems = results[1] as List<ShopItem>;
leaderboard = lbResponse.entries;
currentUserStats = lbResponse.currentUser;
totalParticipants = lbResponse.totalParticipants;
shopItems = results[2] as List<ShopItem>;
// Prefer achievements from dashboard API; fall back to fetched or existing defaults // Prefer achievements from dashboard API; fall back to fetched or existing defaults
final dashAchievements = dashboard.achievements; final dashAchievements = dashboard.achievements;
final fetchedAchievements = results[3] as List<AchievementBadge>; final fetchedAchievements = results[2] as List<AchievementBadge>;
if (dashAchievements.isNotEmpty) { if (dashAchievements.isNotEmpty) {
achievements = dashAchievements; achievements = dashAchievements;
} else if (fetchedAchievements.isNotEmpty) { } else if (fetchedAchievements.isNotEmpty) {
@@ -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 // Change district filter
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Future<void> setDistrict(String district) async { Future<void> setDistrict(String district) async {
if (leaderboardDistrict == district) return; if (leaderboardDistrict == district) return;
leaderboardDistrict = district; leaderboardDistrict = district;
isLeaderboardLoading = true;
notifyListeners(); notifyListeners();
try { try {
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod); final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
@@ -94,6 +112,8 @@ class GamificationProvider extends ChangeNotifier {
totalParticipants = response.totalParticipants; totalParticipants = response.totalParticipants;
} catch (e) { } catch (e) {
error = userFriendlyError(e); error = userFriendlyError(e);
} finally {
isLeaderboardLoading = false;
} }
notifyListeners(); notifyListeners();
} }
@@ -104,6 +124,7 @@ class GamificationProvider extends ChangeNotifier {
Future<void> setTimePeriod(String period) async { Future<void> setTimePeriod(String period) async {
if (leaderboardTimePeriod == period) return; if (leaderboardTimePeriod == period) return;
leaderboardTimePeriod = period; leaderboardTimePeriod = period;
isLeaderboardLoading = true;
notifyListeners(); notifyListeners();
try { try {
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period); final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
@@ -112,6 +133,8 @@ class GamificationProvider extends ChangeNotifier {
totalParticipants = response.totalParticipants; totalParticipants = response.totalParticipants;
} catch (e) { } catch (e) {
error = userFriendlyError(e); error = userFriendlyError(e);
} finally {
isLeaderboardLoading = false;
} }
notifyListeners(); notifyListeners();
} }

View File

@@ -104,7 +104,9 @@ class _ContributeScreenState extends State<ContributeScreen>
super.initState(); super.initState();
PostHogService.instance.screen('Contribute'); PostHogService.instance.screen('Contribute');
WidgetsBinding.instance.addPostFrameCallback((_) { 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), const SizedBox(height: 16),
// Leaderboard List // Leaderboard List
if (provider.isLoading && leaderboard.isEmpty) if (provider.isLeaderboardLoading && leaderboard.isEmpty)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 60), padding: EdgeInsets.symmetric(vertical: 60),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)), child: Center(child: CircularProgressIndicator(strokeWidth: 2)),