Menu
CoddyTech

Pelacak Kebiasaan: Rentetan Hari

Termasuk bagian Proyek dalam perjalanan Flutter di Coddy. Pelajaran 32 dari 36.

Rangkaian hari berturut-turut membuat orang ingin terus kembali: hari-hari berturut-turut terasa seperti sesuatu yang perlu dijaga. Di sini, rangkaian hari adalah rentang hari berstatus selesai terpanjang dalam seminggu. Minum air, selesai dari Senin hingga Rabu dan pada Jumat, memiliki rangkaian selama 3 hari.

Menghitung rangkaian berarti menelusuri hari secara berurutan dengan dua angka: run, jumlah hari berturut-turut sejauh ini, dan best, rangkaian terpanjang yang ditemukan. Hari berstatus selesai menambah jumlah rangkaian; hari terbuka mengatur ulang jumlahnya menjadi 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;
}

Kembalikan best, bukan run: di akhir perulangan, run hanya mencakup rentang terakhir, yang bernilai 0 jika hari Minggu terbuka.

Tampilkan di bawah minggu dengan ikon api, berwarna oranye saat ada rangkaian dan abu-abu outline dari tema saat tidak ada. "Rangkaian 0 hari" terdengar janggal, jadi katakan "Belum ada rangkaian".

challenge icon

Tantangan

Sulit

Tampilkan rentetan:

  • tambahkan getter streak ke Habit
  • di bawah minggu, setelah 12 piksel, tambahkan Row: Icons.local_fire_department dengan ukuran 20 (berwarna oranye saat rentetan lebih dari 0, jika tidak gunakan outline dari tema), 4 piksel, dan teks
  • teksnya adalah 3 day streak (dengan angka sebenarnya), atau No streak yet untuk 0

Coba sendiri

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

Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.

Semua pelajaran di Proyek

Latihan sendiri: Compiler Flutter online