Menu
CoddyTech

Theo dõi thói quen: Số ngày hoàn thành

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

Bảy hình tròn dễ đọc, nhưng một con số sẽ nhanh hơn: "4 trong 7 ngày". Con số này được tính từ các ngày, nên nó thuộc về Habit dưới dạng getter:

int get done => days.where((day) => day).length;

where giữ lại những phần tử vượt qua phép kiểm tra. Ở đây, phép kiểm tra là chính ngày đó, vì mỗi ngày đã là true hoặc false.

Đặt số đếm trên cùng dòng với tên, ở bên phải. Một Row có tên nằm trong Expanded sẽ đẩy số đếm về cuối, còn tên dài sẽ xuống dòng thay vì đẩy số đếm ra khỏi thẻ.

Việc đánh dấu đã gọi setState trên màn hình, nên mọi thẻ đều được dựng lại và mọi số đếm được đọc lại. Không cần kết nối thêm gì.

challenge icon

Thử thách

Dễ

Đếm số ngày đã hoàn thành:

  • thêm getter done vào Habit
  • thay tên trong HabitCard bằng một Row: tên nằm trong Expanded, tiếp theo là 4 of 7 days (với số thực tế) trong bodySmall của giao diện

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

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

  @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,
              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: [
            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],
                    onTap: () => onToggle(i),
                  ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

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