Menu
CoddyTech

Theo dõi thói quen: Chuỗi ngày liên tiếp

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

Streak khiến mọi người muốn quay lại: số ngày liên tiếp tạo cảm giác như một điều cần giữ gìn. Ở đây, streak là số ngày đã hoàn thành liên tiếp dài nhất trong tuần. Uống nước, hoàn thành từ thứ Hai đến thứ Tư và vào thứ Sáu, có streak là 3.

Để đếm số ngày liên tiếp, hãy lần lượt xét từng ngày bằng hai số: run, số ngày liên tiếp tính đến thời điểm hiện tại, và best, số ngày liên tiếp dài nhất đã ghi nhận. Ngày đã hoàn thành làm tăng run; ngày chưa hoàn thành đặt lại giá trị này thành 0.

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

Trả về best, không phải run: khi vòng lặp kết thúc, run chỉ là chuỗi ngày liên tiếp cuối cùng, bằng 0 nếu Chủ nhật chưa hoàn thành.

Hiển thị streak bên dưới tuần cùng biểu tượng ngọn lửa, màu cam khi có streak và màu xám outline theo chủ đề khi không có streak. Cụm “streak 0 ngày” nghe không tự nhiên, nên lúc đó hãy ghi “Chưa có streak”.

challenge icon

Thử thách

Khó

Hiển thị streak:

  • thêm getter streak vào Habit
  • bên dưới tuần, sau 12 pixel, thêm một Row: Icons.local_fire_department ở kích thước 20 (màu cam khi streak lớn hơn 0, nếu không thì dùng outline của chủ đề), 4 pixel và phần văn bản
  • phần văn bản là 3 day streak (với số thực tế), hoặc No streak yet khi giá trị là 0

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;

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

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

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