Menu
CoddyTech

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.

challenge icon

Aufgabe

Schwer

Füge den Dunkelmodus hinzu:

  • mache MyApp zu einem StatefulWidget mit bool _dark = false, gib MaterialApp ein darkTheme mit demselben grünen Seed und Brightness.dark und setze themeMode anhand von _dark
  • gib HabitScreen ein erforderliches bool dark und VoidCallback onToggleTheme; MyApp übergibt _dark und eine Funktion, die den Wert in setState umschaltet
  • in der actions-Liste der oberen Leiste zeigt ein IconButton im hellen Modus Icons.dark_mode und im dunklen Modus Icons.light_mode an und ruft widget.onToggleTheme auf

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,
          ),
        ),
      ),
    );
  }
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Projekte

Übe selbstständig: Online-Flutter-Compiler