Menu
CoddyTech

Theo dõi thói quen: Đánh dấu một ngày

Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 30 trên 36.

Chạm vào một hình tròn để đánh dấu ngày đó, chạm lại để bỏ đánh dấu. Dữ liệu thói quen nằm trong trạng thái của màn hình, nhưng hình tròn được vẽ sâu bên trong HabitCard. Thao tác chạm phải truyền lên nơi lưu dữ liệu.

Đó là cách đưa trạng thái lên trên bằng callback. Màn hình quản lý thay đổi:

void _toggle(Habit habit, int day) {
  setState(() {
    habit.days[day] = !habit.days[day];
  });
}

Màn hình truyền cho mỗi thẻ một hàm, onToggle: (day) => _toggle(habit, day). Kiểu của hàm này trong thẻ là void Function(int day), một hàm nhận số ngày. Mỗi hình tròn nhận một VoidCallback đơn giản, onTap: () => onToggle(i), và bọc Container của nó trong một GestureDetector để gọi hàm đó.

!habit.days[day] đảo giá trị bool: true thành false và ngược lại, nên một hàm vừa đánh dấu vừa bỏ đánh dấu.

challenge icon

Thử thách

Khó

Đánh dấu các ngày:

  • thêm phương thức _toggle vào trạng thái của màn hình
  • thêm tham số bắt buộc onToggle có kiểu void Function(int day) cho HabitCard, rồi truyền (day) => _toggle(habit, day)
  • thêm tham số bắt buộc VoidCallback onTap cho DayCircle, bọc Container bằng GestureDetector, rồi truyền () => onToggle(i)

Tự làm thử

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Habits')),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 8, 16, 88),
        children: [
          for (Habit habit in _habits) HabitCard(habit: habit),
        ],
      ),
    );
  }
}

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

  final Habit habit;

  @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: [
            Text(habit.name, style: textTheme.titleMedium),
            const SizedBox(height: 12),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                for (int i = 0; i < 7; i++) DayCircle(label: dayNames[i], done: habit.days[i]),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class DayCircle extends StatelessWidget {
  const DayCircle({super.key, required this.label, required this.done});

  final String label;
  final bool done;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    return 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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Dự án

Tự luyện tập: Trình biên dịch Flutter online