Create a leaderboard with Riverpod 2 and Isar
As shared with you last week, I want to create a simple game with Flutter. If you haven't seen the first article of this series of articles, you can find it here:
But even if I loved how the game was playable, I couldn't challenge my friends to beat me. They were no leaderboard, and my memory isn't what it used to be 👴
Before taking the game online, I wanted to have a local leaderboard. After carefully reviewing all the solutions, I decided to go with Isar, mainly for its cross-platform support.
To provide isar to my game, I'm going to use Riverpod.
Creating the model
As the leaderboard is going to be pretty simple, I'm going to define the schema for my Scores:
part 'score.g.dart';
@collection
class Score {
Id? id;
late String name;
@Index()
late int score;
@override
String toString() {
return "Score(name: $name, score: $score)";
}
}
Once the build_runner
has run, you should be able to provide the schema to your Isar instance.
Isar.open([ScoreSchema])
Since we're going to access the Isar instance from different providers, we're going to create our first Riverpod provider:
part 'provider.g.dart';
@Riverpod(keepAlive: true)
Future<Isar> isarInstance(FutureProviderRef ref) {
return Isar.open([ScoreSchema]);
}
We're using the new syntax for Riverpod 2, with generated code here. The main benefit is having hot-reload thanks to the generated code.
Creating a score manager
Now we will need to handle the different interactions with the Isar Score collection.
We'll need something to add a score and retrieve the previous scores (pretty simple).
class ScoreManager {
final Isar isar;
ScoreManager(this.isar);
Future<void> addScore(String name, int score) async {
final newScore = Score()
..name = name
..score = score;
await isar.writeTxn(() async {
await isar.scores.put(newScore);
});
}
Future<List<Score>> getScores() async {
return isar.scores.where().sortByScoreDesc().findAll();
}
Future<List<Score>> getHighScores() async {
return isar.scores.where().sortByScoreDesc().limit(8).findAll();
}
}
As you can see, to create this ScoreManager
I need the Isar instance. So I'll use Riverpod once again to provide ScoreManager
.
@riverpod
Future<ScoreManager> scoreManager(ScoreManagerRef ref) async {
final isar = await ref.watch(isarInstanceProvider.future);
return ScoreManager(isar);
}
Then we'll create two providers that will be responsible of giving all the scores and the high scores:
@riverpod
Future<List<Score>> scores(ScoresRef ref) async {
final scoreManager = await ref.watch(scoreManagerProvider.future);
return scoreManager.getScores();
}
@riverpod
Future<List<Score>> highScores(ScoresRef ref) async {
final scoreManager = await ref.watch(scoreManagerProvider.future);
return scoreManager.getHighScores();
}
Till now, we haven't plugged anything into the UI, it's only business logic detached from the UI, thanks to Riverpod!
Displaying the high scores
Now that we have all the providers, we can finally display the scores:
import 'package:boun_twee/models/score.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class HighScorePage extends ConsumerWidget {
const HighScorePage({super.key});
@override
Widget build(BuildContext context, ref) {
final scores = ref.watch(highScoresProvider);
return Scaffold(
body: Center(
child: scores.when(
data: ((scores) {
return Column(
mainAxisSize: MainAxisSize.min,
children: scores.map(
((score) {
return Text(
"${score.name} - ${score.score}",
style: const TextStyle(
fontSize: 24,
color: Colors.black,
),
);
}),
).toList(),
);
}),
loading: (() => const CircularProgressIndicator()),
error: ((error, stack) => Text(error.toString())),
),
),
);
}
}
As you can see, we don't need any StatefulWidget
since riverpod is handling all the error management for us.
Conclusion
In conclusion, we've seen that you can have a locally persisted leaderboard accessible anywhere in your app with very few lines of code.
To not miss the next article of this series, be sure to subscribe to my newsletter. No spam, just my new articles! 🚀
Below you'll find a small code example exclusive to free members.