Menu
CoddyTech

מעקב הרגלים: מחיקת הרגל

חלק מהיחידה פרויקטים במסלול ה-Flutter של Coddy. שיעור 34 מתוך 36.

גם להרגלים יש סוף. לחצו לחיצה ממושכת על כרטיס כדי למחוק אותו. שבוע שלם של סימונים הוא דבר שקשה לאבד בטעות, לכן האפליקציה שואלת קודם.

רכיב InkWell בתוך הכרטיס קולט את הלחיצה באמצעות onLongPress ומצייר את אפקט האדווה. לעיגולי הימים יש הקשות משלהם: הקשה קצרה עדיין מגיעה אל GestureDetector, ורק לחיצה ממושכת מגיעה אל הכרטיס.

השאלה מוצגת בתיבת דו־שיח שמחזירה true או false, כמו בשיעור הקודם. הכותרת שלה מציינת את שם ההרגל, כדי שתדעו מה אתם מוחקים:

final delete = await showDialog<bool>(
  context: context,
  builder: (context) => AlertDialog(
    title: Text('Delete ${habit.name}?'),
    ...
  ),
);
if (delete != true) return;

הכרטיס מעביר את הלחיצה הממושכת באמצעות קריאה חוזרת שלישית, onLongPress, באותו אופן שבו הוא מעביר הקשות על הימים.

challenge icon

אתגר

בינוני

מחיקת הרגלים:

  • כתבו Future<void> _deleteHabit(Habit habit) async: חלון AlertDialog עם הכותרת Delete Stretch? (עם שם ההרגל), השורה Its ticks for this week go with it., כפתור TextButton עם הכיתוב Cancel וכפתור FilledButton עם הכיתוב Delete; בעת לחיצה על Delete, הסירו את ההרגל
  • הוסיפו ל־HabitCard את required VoidCallback onLongPress, והעבירו () => _deleteHabit(habit)
  • בתוך הכרטיס, עטפו את Padding ב־InkWell עם onLongPress הזה, והגדירו ב־Card את clipBehavior: Clip.antiAlias

נסו בעצמכם

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)),
  ];

  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)));
    });
  }

  @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: [
          for (Habit habit in _habits)
            HabitCard(
              habit: habit,
              onToggle: (day) => _toggle(habit, day),
            ),
        ],
      ),
    );
  }
}

class HabitCard extends StatelessWidget {
  const HabitCard({super.key, required this.habit, required this.onToggle});

  final Habit habit;
  final void Function(int day) onToggle;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Card(
      margin: const EdgeInsets.only(top: 12),
      clipBehavior: Clip.antiAlias,
      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 iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה פרויקטים

תרגלו בעצמכם: קומפיילר Flutter אונליין