מעקב הרגלים: מחיקת הרגל
חלק מהיחידה פרויקטים במסלול ה-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, באותו אופן שבו הוא מעביר הקשות על הימים.
אתגר
בינונימחיקת הרגלים:
- כתבו
Future<void> _deleteHabit(Habit habit) async: חלוןAlertDialogעם הכותרתDelete Stretch?(עם שם ההרגל), השורהIts ticks for this week go with it., כפתורTextButtonעם הכיתובCancelוכפתורFilledButtonעם הכיתובDelete; בעת לחיצה על Delete, הסירו את ההרגל - הוסיפו ל־
HabitCardאתrequiredVoidCallback 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,
),
),
),
);
}
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה פרויקטים
תרגלו בעצמכם: קומפיילר Flutter אונליין