gamification_ui 0.0.1
gamification_ui: ^0.0.1 copied to clipboard
Headless gamification UI components for Flutter: achievements, streaks, points, and leaderboards. Bring your own state — no storage, no logic, just widgets.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:gamification_ui/gamification_ui.dart';
void main() => runApp(const GalleryApp());
/// Allows drag-scrolling with a mouse so the web demo feels like mobile.
class _AnyDeviceScrollBehavior extends MaterialScrollBehavior {
const _AnyDeviceScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => PointerDeviceKind.values.toSet();
}
/// Gallery of every gamification_ui component.
class GalleryApp extends StatelessWidget {
/// Creates the gallery app.
const GalleryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'gamification_ui gallery',
scrollBehavior: const _AnyDeviceScrollBehavior(),
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
),
home: const GalleryPage(),
);
}
}
/// Scrollable gallery page.
class GalleryPage extends StatefulWidget {
/// Creates the gallery page.
const GalleryPage({super.key});
@override
State<GalleryPage> createState() => _GalleryPageState();
}
class _GalleryPageState extends State<GalleryPage> {
final ConfettiController _confetti = ConfettiController();
int _points = 2450;
int _xp = 340;
int _streak = 12;
late List<Achievement> _achievements = [
Achievement(
id: 'first_steps',
title: 'First steps',
description: 'Complete your first lesson',
icon: Icons.flag,
unlocked: true,
unlockedAt: DateTime(2026, 6, 21, 18, 30),
),
const Achievement(
id: 'bookworm',
title: 'Bookworm',
description: 'Finish 5 lessons',
icon: Icons.menu_book,
current: 3,
target: 5,
),
const Achievement(
id: 'socialite',
title: 'Socialite',
description: 'Cheer 10 friends',
icon: Icons.chat_bubble,
current: 4,
target: 10,
),
const Achievement(
id: 'week_streak',
title: 'On fire',
description: 'Keep a 7-day streak',
icon: Icons.local_fire_department,
current: 0,
target: 7,
),
const Achievement(
id: 'night_owl',
title: 'Night owl',
description: 'Practice after midnight',
icon: Icons.nightlight,
),
const Achievement(
id: 'century',
title: 'Century',
description: 'Complete 100 lessons',
icon: Icons.emoji_events,
current: 12,
target: 100,
),
];
final List<PointsTransaction> _history = [
PointsTransaction(
label: 'Weekly quest complete',
amount: 120,
timestamp: DateTime(2026, 7, 2, 9, 30),
),
PointsTransaction(
label: 'Theme unlock',
amount: -50,
timestamp: DateTime(2026, 7, 1, 18, 5),
icon: Icons.palette,
),
PointsTransaction(
label: 'Daily login',
amount: 10,
timestamp: DateTime(2026, 7, 1, 8, 0),
),
];
static const _leaders = [
LeaderboardEntry(rank: 1, name: 'Aiko', score: 9800, rankDelta: 2),
LeaderboardEntry(rank: 2, name: 'Ben', score: 9200, rankDelta: -1),
LeaderboardEntry(rank: 3, name: 'Chika', score: 8700, rankDelta: 0),
LeaderboardEntry(
rank: 4,
name: 'You',
score: 8100,
isCurrentUser: true,
rankDelta: 3,
),
LeaderboardEntry(rank: 5, name: 'Emi', score: 7600, rankDelta: -2),
];
void _unlockNext() {
final index = _achievements.indexWhere((a) => !a.unlocked);
if (index == -1) return;
final unlocked = _achievements[index].copyWith(
unlocked: true,
unlockedAt: DateTime.now(),
);
setState(() {
_achievements = [..._achievements]..[index] = unlocked;
_points += 50;
});
_confetti.burst();
showAchievementUnlockToast(context, achievement: unlocked);
}
@override
Widget build(BuildContext context) {
return ConfettiOverlay(
controller: _confetti,
child: Scaffold(
appBar: AppBar(
title: const Text('gamification_ui'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: Center(
child: PointsBalanceChip(
points: _points,
onTap: () => setState(() => _points += 25),
),
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_Section(
title: 'Points',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LevelCard(
level: 12,
currentXp: _xp,
requiredXp: 500,
subtitle: 'Rising star',
),
const SizedBox(height: 12),
Row(
children: [
FilledButton.tonal(
onPressed: () => setState(() => _xp =
_xp + 40 > 500 ? 500 : _xp + 40),
child: const Text('+40 XP'),
),
const SizedBox(width: 8),
FilledButton.tonal(
onPressed: () => setState(() => _points += 100),
child: const Text('+100 pts'),
),
const SizedBox(width: 16),
AnimatedPointsCounter(
value: _points,
style: Theme.of(context).textTheme.titleLarge,
),
],
),
const SizedBox(height: 16),
const Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TierBadge(tier: Tier.bronze, size: 56),
TierBadge(tier: Tier.silver, size: 56),
TierBadge(tier: Tier.gold, size: 56),
TierBadge(tier: Tier.platinum, size: 56),
TierBadge(tier: Tier.diamond, size: 56),
],
),
],
),
),
_Section(
title: 'Achievements',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AchievementBadgeGrid(
achievements: _achievements,
onTap: (a) => showAchievementDetailSheet(
context,
achievement: a,
),
),
const SizedBox(height: 8),
FilledButton.icon(
onPressed: _unlockNext,
icon: const Icon(Icons.celebration),
label: const Text('Unlock next'),
),
const SizedBox(height: 12),
AchievementProgressCard(achievement: _achievements[1]),
],
),
),
_Section(
title: 'Streaks',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
StreakFlameCounter(count: _streak),
const Spacer(),
IconButton(
onPressed: () => setState(
() => _streak = _streak > 0 ? _streak - 1 : 0,
),
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: () => setState(() => _streak++),
icon: const Icon(Icons.add),
),
],
),
const SizedBox(height: 12),
const StreakWeeklyCalendar(
completed: [true, true, true, false, false, false, false],
todayIndex: 3,
),
const SizedBox(height: 16),
StreakMilestoneBar(current: _streak),
],
),
),
_Section(
title: 'Leaderboard',
child: Column(
children: [
const LeaderboardPodium(entries: _leaders),
const SizedBox(height: 8),
LeaderboardList(entries: _leaders),
],
),
),
_Section(
title: 'History',
child: PointsHistoryList(transactions: _history),
),
const SizedBox(height: 48),
],
),
),
);
}
}
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
child,
],
),
);
}
}