Suivi des habitudes : Thème clair et sombre
Fait partie de la section Projets du Journey Flutter de Coddy. Leçon 36 sur 36.
Les utilisateurs consultent leurs habitudes tard le soir ; la dernière fonctionnalité est donc le mode sombre, avec un bouton dans la barre supérieure : une lune pour l’activer, un soleil pour le désactiver.
Le thème se trouve dans MaterialApp, le bouton aussi : MyApp devient un StatefulWidget avec un bool _dark. MaterialApp reçoit les deux thèmes et choisit l’un d’eux avec themeMode :
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
),
themeMode: _dark ? ThemeMode.dark : ThemeMode.light,Le bouton se trouve dans l’écran des habitudes, bien en dessous de MyApp. Remonte encore l’état : HabitScreen reçoit dark pour choisir son icône et un rappel onToggleTheme à appeler. L’écran les récupère depuis son widget sous la forme de widget.dark et widget.onToggleTheme.
Toutes les couleurs de l’application provenaient du thème, donc les cartes, les cercles et l’anneau changent tous avec lui. Tes habitudes restent cochées, puisqu’elles se trouvent dans l’état de l’écran, et non dans le thème.
C’est la dernière leçon des projets du cours. Les vérifications portent sur le suivi complet : ajouter, cocher, série, supprimer, l’anneau et le mode sombre.
Défi
DifficileAjoutez le mode sombre :
- faites de
MyAppunStatefulWidgetavecbool _dark = false, attribuez àMaterialAppundarkThemeutilisant la même graine verte avecBrightness.dark, et définissezthemeModeà partir de_dark - ajoutez à
HabitScreenunbool darkrequiredet unVoidCallback onToggleTheme;MyApptransmet_darket une fonction qui inverse sa valeur danssetState - dans les
actionsde la barre supérieure, unIconButtonafficheIcons.dark_modeen mode clair etIcons.light_modeen mode sombre, et appellewidget.onToggleTheme
Essayez vous-même
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
),
home: const HabitScreen(),
);
}
}
const List<String> dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
class Habit {
Habit(this.name, this.days);
final String name;
final List<bool> days;
int get done => days.where((day) => day).length;
int get streak {
int best = 0;
int run = 0;
for (bool day in days) {
if (day) {
run++;
if (run > best) best = run;
} else {
run = 0;
}
}
return best;
}
}
class HabitScreen extends StatefulWidget {
const HabitScreen({super.key});
@override
State<HabitScreen> createState() => _HabitScreenState();
}
class _HabitScreenState extends State<HabitScreen> {
final List<Habit> _habits = [
Habit('Drink water', [true, true, true, false, true, false, false]),
Habit('Read 10 pages', [true, false, true, true, false, false, false]),
Habit('Stretch', List.filled(7, false)),
];
int get _ticks => _habits.fold(0, (sum, habit) => sum + habit.done);
int get _possible => _habits.length * 7;
void _toggle(Habit habit, int day) {
setState(() {
habit.days[day] = !habit.days[day];
});
}
Future<void> _addHabit() async {
String name = '';
final added = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('New habit'),
content: TextField(
autofocus: true,
decoration: const InputDecoration(labelText: 'Habit name'),
onChanged: (value) => name = value,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Add'),
),
],
),
);
if (added != true || name.trim().isEmpty) return;
setState(() {
_habits.add(Habit(name.trim(), List.filled(7, false)));
});
}
Future<void> _deleteHabit(Habit habit) async {
final delete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete ${habit.name}?'),
content: const Text('Its ticks for this week go with it.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
if (delete != true) return;
setState(() {
_habits.remove(habit);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Habits')),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: const Icon(Icons.add),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 88),
children: [
ProgressCard(ticks: _ticks, possible: _possible),
for (Habit habit in _habits)
HabitCard(
habit: habit,
onToggle: (day) => _toggle(habit, day),
onLongPress: () => _deleteHabit(habit),
),
],
),
);
}
}
class ProgressCard extends StatelessWidget {
const ProgressCard({super.key, required this.ticks, required this.possible});
final int ticks;
final int possible;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final progress = possible == 0 ? 0.0 : ticks / possible;
return Card(
margin: EdgeInsets.zero,
color: colors.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
SizedBox(
width: 72,
height: 72,
child: Stack(
fit: StackFit.expand,
children: [
CircularProgressIndicator(
value: progress,
strokeWidth: 8,
strokeCap: StrokeCap.round,
backgroundColor: colors.surface,
),
Center(
child: Text(
'${(progress * 100).round()}%',
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('This week', style: textTheme.titleLarge),
const SizedBox(height: 4),
Text('$ticks of $possible ticks'),
],
),
),
],
),
),
);
}
}
class HabitCard extends StatelessWidget {
const HabitCard({
super.key,
required this.habit,
required this.onToggle,
required this.onLongPress,
});
final Habit habit;
final void Function(int day) onToggle;
final VoidCallback onLongPress;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(top: 12),
clipBehavior: Clip.antiAlias,
child: InkWell(
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: Text(habit.name, style: textTheme.titleMedium)),
Text('${habit.done} of 7 days', style: textTheme.bodySmall),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (int i = 0; i < 7; i++)
DayCircle(
label: dayNames[i],
done: habit.days[i],
onTap: () => onToggle(i),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.local_fire_department,
size: 20,
color: habit.streak > 0 ? Colors.orange : Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 4),
Text(habit.streak == 0 ? 'No streak yet' : '${habit.streak} day streak'),
],
),
],
),
),
),
);
}
}
class DayCircle extends StatelessWidget {
const DayCircle({super.key, required this.label, required this.done, required this.onTap});
final String label;
final bool done;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: done ? colors.primary : colors.surfaceContainerHighest,
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: done ? colors.onPrimary : colors.onSurfaceVariant,
),
),
),
);
}
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Projets
1Projet : Liste de tâches
Liste de tâches : La liste des tâchesListe de tâches : Ajouter une tâche4Projet : Suivi des habitudes
Suivi des habitudes : Vos habitudesSuivi des habitudes : La semaineEntraînez-vous par vous-même : Compilateur Flutter en ligne