Gewohnheitstracker: Hell und dunkel
Teil des Abschnitts Projekte der Flutter-Journey von Coddy. Lektion 36 von 36.
Da Menschen ihre Gewohnheiten oft spätabends überprüfen, ist die letzte Funktion der Dunkelmodus mit einer Schaltfläche in der oberen Leiste: ein Mond zum Einschalten, eine Sonne zum Zurückschalten.
Das Design ist in MaterialApp definiert, also gehört auch der Umschalter dorthin: MyApp wird zu einem StatefulWidget mit einem bool _dark. MaterialApp erhält beide Designs und wählt eines davon mit themeMode aus:
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
),
themeMode: _dark ? ThemeMode.dark : ThemeMode.light,Die Schaltfläche befindet sich im Gewohnheitenbildschirm, weit unterhalb von MyApp. Hebe den Zustand noch einmal nach oben: HabitScreen erhält dark, um sein Symbol auszuwählen, und einen onToggleTheme-Callback, den es aufrufen kann. Der Bildschirm liest sie aus seinem Widget als widget.dark und widget.onToggleTheme aus.
Jede Farbe in der App stammte aus dem Design, daher wechseln auch die Karten, die Kreise und der Ring mit ihm. Deine Gewohnheiten bleiben abgehakt, da sie im Zustand des Bildschirms und nicht im Design gespeichert sind.
Dies ist die letzte Lektion der Projektabschnitte des Kurses. Die Prüfungen verwenden den gesamten Tracker: hinzufügen, abhaken, Serie, löschen, der Ring und der Dunkelmodus.
Aufgabe
SchwerFüge den Dunkelmodus hinzu:
- mache
MyAppzu einemStatefulWidgetmitbool _dark = false, gibMaterialAppeindarkThememit demselben grünen Seed undBrightness.darkund setzethemeModeanhand von_dark - gib
HabitScreenein erforderlichesbool darkundVoidCallback onToggleTheme;MyAppübergibt_darkund eine Funktion, die den Wert insetStateumschaltet - in der
actions-Liste der oberen Leiste zeigt einIconButtonim hellen ModusIcons.dark_modeund im dunklen ModusIcons.light_modean und ruftwidget.onToggleThemeauf
Probier es selbst
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,
),
),
),
);
}
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Projekte
1Projekt: To-do-Liste
To-do-Liste: Die AufgabenlisteTo-do-Liste: Eine Aufgabe hinzufügen4Projekt: Gewohnheitstracker
Gewohnheitstracker: Deine GewohnheitenGewohnheitstracker: Die WocheÜbe selbstständig: Online-Flutter-Compiler