firebase-realtime-database-ile-haftalik-liderlik-tablosu-sistemi-

Weekly Leaderboard System with Firebase Realtime Database

Table of Contents

The competitive element in games is one of the most important factors that increase user interaction and motivation. In this article, we will explain in detail how we created our weekly leaderboard system, which is updated every Wednesday in Chipode Sudoku, using Firebase Realtime Database.

System Design

1- Data Structure

				
					json
{
  "leaderboards": {
    "2024-W48": {
      "rankings": {
        "user123": {
          "score": 15420,
          "displayName": "AliceGamer",
          "avatar": "avatar_1",
          "lastUpdated": 1701345678901
        },
        "user456": {
          "score": 14380,
          "displayName": "BobPlayer",
          "avatar": "avatar_3",
          "lastUpdated": 1701345789012
        }
      },
      "startDate": 1701302400000,
      "endDate": 1701907199999
    }
  },
  "users": {
    "user123": {
      "currentWeekScore": 15420,
      "totalScore": 154980,
      "achievements": {
        "weeklyWinner": 3,
        "topPlayer": true
      }
    }
  }
}

				
			

2- Firebase Configuration

				
					dart
class FirebaseConfig {
  static Future<void> initialize() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
      name: 'chipode-sudoku',
    );
    
    if (kDebugMode) {
      // Local emülatör kullanımı
      FirebaseDatabase.instance.useDatabaseEmulator('localhost', 9000);
    }
  }
  
  static DatabaseReference get leaderboardRef => 
      FirebaseDatabase.instance.ref('leaderboards');
      
  static DatabaseReference get usersRef =>
      FirebaseDatabase.instance.ref('users');
}


				
			

Leaderboard Management

1- Leaderboard Service

				
					dart
class LeaderboardService {
  final DatabaseReference _dbRef;
  final String _currentWeek;
  
  LeaderboardService(): 
    _dbRef = FirebaseConfig.leaderboardRef,
    _currentWeek = _calculateCurrentWeek();
  
  static String _calculateCurrentWeek() {
    final now = DateTime.now();
    final weekNumber = weekOfYear(now);
    return '${now.year}-W${weekNumber.toString().padLeft(2, '0')}';
  }
  
  Future<void> submitScore({
    required String userId,
    required int score,
    required String displayName,
    required String avatar,
  }) async {
    final userRef = _dbRef
        .child(_currentWeek)
        .child('rankings')
        .child(userId);
    
    // Transaction kullanarak atomic update
    await userRef.runTransaction((data) {
      if (data == null) {
        return Transaction.success({
          'score': score,
          'displayName': displayName,
          'avatar': avatar,
          'lastUpdated': ServerValue.timestamp,
        });
      }
      
      final currentScore = data['score'] as int;
      if (score > currentScore) {
        data['score'] = score;
        data['lastUpdated'] = ServerValue.timestamp;
      }
      
      return Transaction.success(data);
    });
  }
  
  Stream<List<LeaderboardEntry>> getTopPlayers({int limit = 100}) {
    return _dbRef
        .child(_currentWeek)
        .child('rankings')
        .orderByChild('score')
        .limitToLast(limit)
        .onValue
        .map((event) {
          final data = event.snapshot.value as Map<dynamic, dynamic>;
          
          final entries = data.entries.map((e) => LeaderboardEntry(
            userId: e.key as String,
            score: e.value['score'] as int,
            displayName: e.value['displayName'] as String,
            avatar: e.value['avatar'] as String,
            lastUpdated: DateTime.fromMillisecondsSinceEpoch(
              e.value['lastUpdated'] as int,
            ),
          )).toList();
          
          // Skorlara göre sırala
          entries.sort((a, b) => b.score.compareTo(a.score));
          return entries;
        });
  }
  
  Future<int> getUserRank(String userId) async {
    final snapshot = await _dbRef
        .child(_currentWeek)
        .child('rankings')
        .orderByChild('score')
        .get();
        
    if (!snapshot.exists) return 0;
    
    final data = snapshot.value as Map<dynamic, dynamic>;
    final entries = data.entries.toList();
    entries.sort((a, b) => 
        (b.value['score'] as int).compareTo(a.value['score'] as int));
        
    final index = entries.indexWhere((e) => e.key == userId);
    return index + 1;
  }
}


				
			

2- Weekly Reset System

				
					dart
class WeeklyResetManager {
  final LeaderboardService _leaderboard;
  final AchievementService _achievements;
  Timer? _resetTimer;
  
  void startWeeklyReset() {
    final now = DateTime.now();
    final nextWednesday = _getNextWednesday(now);
    final timeUntilReset = nextWednesday.difference(now);
    
    _resetTimer?.cancel();
    _resetTimer = Timer(timeUntilReset, _performReset);
  }
  
  Future<void> _performReset() async {
    try {
      // Mevcut haftanın kazananlarını belirle
      final winners = await _determineWinners();
      
      // Başarımları dağıt
      await _distributeAchievements(winners);
      
      // Yeni hafta için tabloyu hazırla
      await _prepareNewWeek();
      
      // Bildirim gönder
      await _notifyUsers();
      
      // Bir sonraki reset için zamanlayıcıyı başlat
      startWeeklyReset();
    } catch (e) {
      log.error('Weekly reset failed', e);
      // Retry mekanizması
    }
  }
  
  DateTime _getNextWednesday(DateTime from) {
    var nextWed = from;
    while (nextWed.weekday != DateTime.wednesday) {
      nextWed = nextWed.add(Duration(days: 1));
    }
    return DateTime(
      nextWed.year,
      nextWed.month,
      nextWed.day,
      0, 0, 0,
    );
  }
  
  Future<List<LeaderboardEntry>> _determineWinners() async {
    final currentWeek = await _leaderboard.getTopPlayers(limit: 3);
    return currentWeek;
  }
}



				
			

User Interface

1- Leaderboard Widget

				
					dart
class LeaderboardScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Haftalık Liderlik Tablosu'),
        actions: [
          TimeUntilResetWidget(),
        ],
      ),
      body: Column(
        children: [
          TopPlayersWidget(),
          Expanded(
            child: LeaderboardList(),
          ),
        ],
      ),
    );
  }
}

class LeaderboardList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<LeaderboardService>(
      builder: (context, service, child) {
        return StreamBuilder<List<LeaderboardEntry>>(
          stream: service.getTopPlayers(),
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ErrorWidget(snapshot.error!);
            }
            
            if (!snapshot.hasData) {
              return LoadingIndicator();
            }
            
            final entries = snapshot.data!;
            return ListView.builder(
              itemCount: entries.length,
              itemBuilder: (context, index) {
                final entry = entries[index];
                return LeaderboardEntryTile(
                  rank: index + 1,
                  entry: entry,
                  highlighted: entry.userId == getCurrentUserId(),
                );
              },
            );
          },
        );
      },
    );
  }
}

class LeaderboardEntryTile extends StatelessWidget {
  final int rank;
  final LeaderboardEntry entry;
  final bool highlighted;
  
  @override
  Widget build(BuildContext context) {
    return Container(
      color: highlighted ? Colors.amber.withOpacity(0.1) : null,
      child: ListTile(
        leading: _buildRankWidget(rank),
        title: Text(entry.displayName),
        trailing: Text(
          NumberFormat('#,###').format(entry.score),
          style: TextStyle(
            fontWeight: FontWeight.bold,
            color: Theme.of(context).primaryColor,
          ),
        ),
        subtitle: Text(
          'Son güncelleme: ${timeAgo.format(entry.lastUpdated)}',
        ),
      ),
    );
  }
  
  Widget _buildRankWidget(int rank) {
    if (rank > 3) return Text('#$rank');
    
    final colors = {
      1: Colors.amber,
      2: Colors.grey[400],
      3: Colors.brown[300],
    };
    
    return Icon(
      Icons.emoji_events,
      color: colors[rank],
      size: 32,
    );
  }
}


				
			

Security and Optimization

1- Security Rules

				
					javascript
{
  "rules": {
    "leaderboards": {
      "$week": {
        "rankings": {
          "$userId": {
            // Sadece kendi skorunu güncelleyebilir
            ".write": "auth != null && auth.uid == $userId",
            ".validate": "newData.hasChildren(['score', 'displayName', 'lastUpdated']) && 
                         newData.child('score').isNumber() &&
                         (!data.exists() || newData.child('score').val() > data.child('score').val())"
          },
          // Herkes okuyabilir
          ".read": true,
          // Sıralama için index
          ".indexOn": ["score"]
        }
      }
    }
  }
}

				
			

2- Caching and Performance

				
					dart
class LeaderboardCache {
  final _cache = <String, List<LeaderboardEntry>>{};
  final _expirations = <String, DateTime>{};
  
  static const cacheDuration = Duration(minutes: 5);
  
  List<LeaderboardEntry>? getFromCache(String week) {
    final expiration = _expirations[week];
    if (expiration == null || expiration.isBefore(DateTime.now())) {
      _cache.remove(week);
      _expirations.remove(week);
      return null;
    }
    return _cache[week];
  }
  
  void cacheEntries(String week, List<LeaderboardEntry> entries) {
    _cache[week] = entries;
    _expirations[week] = DateTime.now().add(cacheDuration);
  }
  
  void invalidateCache() {
    _cache.clear();
    _expirations.clear();
  }
}

				
			

3- Error Monitoring

				
					dart
class LeaderboardErrorTracker {
  static void trackError(String operation, dynamic error) {
    FirebaseCrashlytics.instance.recordError(
      error,
      StackTrace.current,
      reason: 'Leaderboard operation failed: $operation',
    );
  }
  
  static void trackLatency(String operation, Duration duration) {
    FirebasePerformance.instance
        .newTrace('leaderboard_$operation')
        .then((trace) async {
          await trace.start();
          await Future.delayed(duration);
          await trace.stop();
        });
  }
}

				
			

Notification System

1- Notification Service

				
					dart
class LeaderboardNotificationService {
  final messaging = FirebaseMessaging.instance;
  
  Future<void> subscribeToUpdates() async {
    await messaging.subscribeToTopic('leaderboard_updates');
  }
  
  Future<void> notifyTopPlayers(List<LeaderboardEntry> winners) async {
    final functions = FirebaseFunctions.instance;
    
    await functions.httpsCallable('notifyLeaderboardWinners').call({
      'winners': winners.map((w) => w.toJson()).toList(),
    });
  }
  
  Future<void> notifyUserRankChange({
    required String userId,
    required int oldRank,
    required int newRank,
  }) async {
    if (oldRank == 0 || newRank >= oldRank) return;
    
    final user = await FirebaseAuth.instance.currentUser;
    if (user?.uid != userId) return;
    
    await _showRankChangeNotification(
      oldRank: oldRank,
      newRank: newRank,
    );
  }
}


				
			

Result

Thanks to the weekly leaderboard system we created using Firebase Realtime Database:

1. Real-time score updates
2. Automatic weekly reset
3. Achievement system integration
4. Notification system
5. Secure and scalable structure

we have achieved.

Future Plans

– Friend-based ranking
– Monthly and seasonal tables
– Special tournaments
– Cross-platform synchronization
– Regional rankings

References

Twitter
LinkedIn
Related Articles
Contact Form

Chipode Apps

loading...
Google Play Store
loading...
Apple Store