Menu
CoddyTech

Pelacak Kebiasaan: Hapus Kebiasaan

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

Kebiasaan juga bisa diakhiri. Tekan dan tahan kartu untuk menghapusnya. Banyak sekali tanda centang selama seminggu yang bisa hilang tanpa sengaja, jadi aplikasi bertanya terlebih dahulu.

InkWell di dalam kartu menangkap tekanan melalui onLongPress dan menggambar efek riak. Lingkaran hari tetap menerima ketukannya sendiri: ketukan singkat tetap diteruskan ke GestureDetector, dan hanya tekanan lama yang diteruskan ke kartu.

Pertanyaan ditampilkan dalam dialog yang menghasilkan true atau false, seperti pada pelajaran sebelumnya. Judulnya menyebutkan nama kebiasaan, jadi kamu tahu apa yang akan kamu hapus:

final delete = await showDialog<bool>(
  context: context,
  builder: (context) => AlertDialog(
    title: Text('Delete ${habit.name}?'),
    ...
  ),
);
if (delete != true) return;

Kartu meneruskan tekanan lama melalui callback ketiga, onLongPress, sama seperti kartu meneruskan ketukan pada hari.

challenge icon

Tantangan

Sedang

Hapus kebiasaan:

  • tulis Future<void> _deleteHabit(Habit habit) async: sebuah AlertDialog berjudul Delete Stretch? (dengan nama kebiasaan), baris Its ticks for this week go with it., sebuah Cancel TextButton dan sebuah Delete FilledButton; saat Delete ditekan, hapus kebiasaan tersebut
  • beri HabitCard required VoidCallback onLongPress, lalu teruskan () => _deleteHabit(habit)
  • di dalam kartu, letakkan InkWell dengan onLongPress tersebut mengelilingi Padding, dan beri Card clipBehavior: Clip.antiAlias

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;

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