Flutter

Project Flutter Mobile dengan Vibe Coding: Habit Tracker

Tutorial project Flutter Mobile Habit Tracker dengan pendekatan vibe coding: ide, MVP, prompt, struktur folder, model Habit, local storage, UI, progress harian, debugging, dan checklist testing.

30 dari 58 materi Project fluttervibe-codinghabit-trackerprojectmobilelocal-storage

Project Flutter Mobile dengan Vibe Coding: Habit Tracker

Di materi ini kita akan membuat project Flutter Mobile bernama Habit Tracker dengan pendekatan vibe coding.

Habit Tracker adalah aplikasi untuk mencatat kebiasaan harian. Contoh habit:

  • minum air 8 gelas
  • membaca buku
  • olahraga
  • belajar Flutter
  • tidur sebelum jam 11 malam

Project ini cocok untuk latihan karena fiturnya sederhana, tetapi konsepnya banyak:

  • model data
  • form input
  • list data
  • toggle status harian
  • local storage
  • state lokal
  • refactor service
  • review kode AI
  • checklist testing

Targetnya bukan langsung membuat aplikasi sempurna. Targetnya adalah belajar alur membuat aplikasi mobile dari nol sampai bisa dipakai.


1. Hasil Akhir yang Ingin Dibuat

Fitur versi pertama:

  • user bisa menambah habit
  • user bisa melihat daftar habit
  • user bisa menandai habit selesai hari ini
  • user bisa menghapus habit
  • user bisa melihat jumlah habit selesai hari ini
  • data tersimpan walaupun aplikasi ditutup

Contoh tampilan sederhana:

Habit Tracker

Hari ini
3 dari 5 habit selesai

[+] Tambah Habit

[x] Minum air
[ ] Belajar Flutter
[x] Baca buku
[ ] Olahraga
[x] Tidur cukup

Setelah versi ini selesai, nanti bisa dikembangkan menjadi:

  • streak harian
  • kalender habit
  • statistik mingguan
  • reminder
  • Firebase sync
  • login user

2. Cara Vibe Coding untuk Project Ini

Kita tidak akan meminta AI membuat semuanya sekaligus.

Kita pakai pendekatan bertahap:

Tahap 1 - Definisikan fitur MVP
Tahap 2 - Buat model Habit
Tahap 3 - Buat UI list habit
Tahap 4 - Tambah form add habit
Tahap 5 - Tambah toggle selesai hari ini
Tahap 6 - Tambah delete habit
Tahap 7 - Tambah local storage
Tahap 8 - Refactor kode
Tahap 9 - Testing manual
Tahap 10 - Catatan dokumentasi

Kenapa bertahap?

Karena kalau semua dibuat sekaligus:

  • sulit membaca kode
  • sulit tahu error dari bagian mana
  • AI bisa membuat struktur terlalu kompleks
  • pemula mudah hanya copy-paste

Vibe coding yang bagus adalah memberi instruksi kecil, menjalankan hasilnya, lalu lanjut.


3. Prompt Awal Project

Gunakan prompt ini untuk memulai project dengan AI:

Saya ingin membuat project Flutter Mobile untuk belajar vibe coding.
Project: Habit Tracker.

Level saya masih belajar Flutter.

Target versi MVP:
- tambah habit
- tampilkan daftar habit
- tandai habit selesai hari ini
- hapus habit
- simpan data lokal
- tampilkan progress selesai hari ini

Tolong bantu saya secara bertahap.
Jangan langsung buat semua kode.
Mulai dari:
1. daftar fitur MVP
2. struktur folder sederhana
3. model Habit
4. checklist testing

Prompt ini bagus karena:

  • menjelaskan project
  • menjelaskan level
  • menjelaskan fitur
  • meminta step-by-step
  • membatasi agar AI tidak langsung terlalu besar

4. Membuat Project Flutter

Command:

flutter create habit_tracker
cd habit_tracker
flutter run

Setelah project berjalan, bersihkan lib/main.dart agar siap dipakai.

Prompt ke AI:

Buatkan main.dart Flutter sederhana untuk project Habit Tracker.
Gunakan MaterialApp, debugShowCheckedModeBanner false,
theme warna hijau atau biru, dan home HabitPage.
Jangan buat fitur dulu, cukup struktur awal.

Contoh main.dart:

import 'package:flutter/material.dart';

import 'pages/habit_page.dart';

void main() {
  runApp(const HabitTrackerApp());
}

class HabitTrackerApp extends StatelessWidget {
  const HabitTrackerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Habit Tracker',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HabitPage(),
    );
  }
}

5. Struktur Folder

Untuk pemula, gunakan struktur sederhana:

lib/
  main.dart
  models/
    habit.dart
  pages/
    habit_page.dart
  services/
    habit_storage_service.dart

Penjelasan:

  • main.dart: titik awal aplikasi
  • models/habit.dart: bentuk data habit
  • pages/habit_page.dart: tampilan utama aplikasi
  • services/habit_storage_service.dart: simpan dan baca data lokal

Struktur ini cukup rapi tanpa terlalu rumit.


6. Membuat Model Habit

File:

lib/models/habit.dart

Isi:

class Habit {
  final String id;
  final String title;
  final bool isDoneToday;
  final DateTime createdAt;
  final DateTime? lastCompletedAt;

  const Habit({
    required this.id,
    required this.title,
    required this.isDoneToday,
    required this.createdAt,
    this.lastCompletedAt,
  });

  Habit copyWith({
    String? id,
    String? title,
    bool? isDoneToday,
    DateTime? createdAt,
    DateTime? lastCompletedAt,
  }) {
    return Habit(
      id: id ?? this.id,
      title: title ?? this.title,
      isDoneToday: isDoneToday ?? this.isDoneToday,
      createdAt: createdAt ?? this.createdAt,
      lastCompletedAt: lastCompletedAt ?? this.lastCompletedAt,
    );
  }
}

Penjelasan:

  • id: identitas unik habit
  • title: nama habit
  • isDoneToday: status selesai hari ini
  • createdAt: kapan habit dibuat
  • lastCompletedAt: kapan terakhir habit diselesaikan
  • copyWith: membuat object baru dari data lama

Kenapa perlu copyWith?

Karena di Flutter lebih rapi jika data dibuat sebagai object immutable. Saat ingin mengubah status, kita membuat object baru.

Contoh:

final updatedHabit = habit.copyWith(isDoneToday: true);

7. Prompt untuk Memahami Model

Setelah AI membuat model, jangan langsung lanjut. Minta penjelasan.

Prompt:

Jelaskan model Habit ini untuk pemula.
Bahas:
- fungsi setiap field
- kenapa id dibutuhkan
- kenapa pakai final
- kenapa copyWith berguna
- contoh cara membuat Habit baru
- contoh cara toggle isDoneToday

Ini penting agar kamu tidak hanya memakai model, tetapi paham kenapa bentuknya seperti itu.


8. Membuat UI Awal HabitPage

File:

lib/pages/habit_page.dart

Isi awal:

import 'package:flutter/material.dart';

import '../models/habit.dart';

class HabitPage extends StatefulWidget {
  const HabitPage({super.key});

  @override
  State<HabitPage> createState() => _HabitPageState();
}

class _HabitPageState extends State<HabitPage> {
  final List<Habit> habits = [];

  int get completedCount {
    return habits.where((habit) => habit.isDoneToday).length;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Habit Tracker'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Hari ini',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 8),
            Text(
              '$completedCount dari ${habits.length} habit selesai',
            ),
            const SizedBox(height: 16),
            Expanded(
              child: habits.isEmpty
                  ? const Center(
                      child: Text('Belum ada habit'),
                    )
                  : ListView.builder(
                      itemCount: habits.length,
                      itemBuilder: (context, index) {
                        final habit = habits[index];

                        return ListTile(
                          title: Text(habit.title),
                          leading: Checkbox(
                            value: habit.isDoneToday,
                            onChanged: null,
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

Saat ini belum ada fitur tambah habit. Kita hanya menyiapkan tampilan list.

Checklist:

  • aplikasi bisa dijalankan
  • halaman Habit Tracker muncul
  • empty state muncul
  • tidak ada error import

9. Menambah Data Dummy

Sebelum membuat form, gunakan data dummy agar UI terlihat.

Ubah list:

final List<Habit> habits = [
  Habit(
    id: '1',
    title: 'Belajar Flutter',
    isDoneToday: false,
    createdAt: DateTime.now(),
  ),
  Habit(
    id: '2',
    title: 'Minum air',
    isDoneToday: true,
    createdAt: DateTime.now(),
    lastCompletedAt: DateTime.now(),
  ),
];

Tujuannya:

  • melihat tampilan list
  • melihat progress berubah
  • mengecek checkbox
  • memastikan layout tidak kosong

Setelah UI aman, data dummy nanti dihapus.


10. Menambah Form Add Habit

Kita buat input sederhana di bagian atas.

Tambahkan controller:

final TextEditingController habitController = TextEditingController();

Tambahkan dispose:

@override
void dispose() {
  habitController.dispose();
  super.dispose();
}

Tambahkan function:

void addHabit() {
  final title = habitController.text.trim();

  if (title.isEmpty) return;

  final habit = Habit(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    title: title,
    isDoneToday: false,
    createdAt: DateTime.now(),
  );

  setState(() {
    habits.add(habit);
    habitController.clear();
  });
}

Tambahkan UI form sebelum list:

Row(
  children: [
    Expanded(
      child: TextField(
        controller: habitController,
        decoration: const InputDecoration(
          labelText: 'Habit baru',
          border: OutlineInputBorder(),
        ),
        onSubmitted: (_) => addHabit(),
      ),
    ),
    const SizedBox(width: 8),
    ElevatedButton(
      onPressed: addHabit,
      child: const Text('Tambah'),
    ),
  ],
),
const SizedBox(height: 16),

Penjelasan:

  • TextEditingController membaca input user
  • trim menghapus spasi depan belakang
  • input kosong tidak ditambahkan
  • DateTime.now().millisecondsSinceEpoch dipakai sebagai id sederhana
  • setState membuat UI update

11. Prompt untuk Fitur Add Habit

Prompt:

Saya sudah punya HabitPage dengan List<Habit>.
Bantu tambahkan fitur add habit.

Syarat:
- pakai TextEditingController
- input kosong tidak boleh masuk
- setelah tambah, input dikosongkan
- id dibuat sederhana dari DateTime.now().millisecondsSinceEpoch
- jelaskan kenapa harus pakai setState

Setelah AI memberi kode, cek:

  • controller dibuat
  • dispose dipanggil
  • setState dipakai
  • tidak ada habit kosong
  • UI tetap rapi

12. Toggle Habit Selesai Hari Ini

Tambahkan function:

void toggleHabit(int index) {
  final habit = habits[index];
  final isDone = !habit.isDoneToday;

  setState(() {
    habits[index] = habit.copyWith(
      isDoneToday: isDone,
      lastCompletedAt: isDone ? DateTime.now() : null,
    );
  });
}

Ubah checkbox:

Checkbox(
  value: habit.isDoneToday,
  onChanged: (_) => toggleHabit(index),
),

Ubah title agar habit selesai terlihat dicoret:

Text(
  habit.title,
  style: TextStyle(
    decoration: habit.isDoneToday
        ? TextDecoration.lineThrough
        : TextDecoration.none,
  ),
),

Penjelasan:

  • toggleHabit membalik nilai isDoneToday
  • jika selesai, lastCompletedAt diisi waktu sekarang
  • jika dibatalkan, lastCompletedAt dikosongkan
  • progress otomatis berubah karena completedCount membaca dari list terbaru

13. Menambah Delete Habit

Tambahkan function:

void deleteHabit(int index) {
  setState(() {
    habits.removeAt(index);
  });
}

Tambahkan tombol delete:

trailing: IconButton(
  icon: const Icon(Icons.delete),
  onPressed: () => deleteHabit(index),
),

Untuk UX yang lebih aman, bisa tambah dialog konfirmasi.

Prompt:

Tambahkan fitur delete habit dengan dialog konfirmasi.
Jika user pilih batal, data tidak dihapus.
Jika pilih hapus, habit dihapus dari list.
Gunakan AlertDialog yang sederhana.

14. Kode HabitPage Sementara

Setelah tahap add, toggle, dan delete, bentuk HabitPage bisa seperti ini:

import 'package:flutter/material.dart';

import '../models/habit.dart';

class HabitPage extends StatefulWidget {
  const HabitPage({super.key});

  @override
  State<HabitPage> createState() => _HabitPageState();
}

class _HabitPageState extends State<HabitPage> {
  final List<Habit> habits = [];
  final TextEditingController habitController = TextEditingController();

  int get completedCount {
    return habits.where((habit) => habit.isDoneToday).length;
  }

  @override
  void dispose() {
    habitController.dispose();
    super.dispose();
  }

  void addHabit() {
    final title = habitController.text.trim();

    if (title.isEmpty) return;

    final habit = Habit(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title,
      isDoneToday: false,
      createdAt: DateTime.now(),
    );

    setState(() {
      habits.add(habit);
      habitController.clear();
    });
  }

  void toggleHabit(int index) {
    final habit = habits[index];
    final isDone = !habit.isDoneToday;

    setState(() {
      habits[index] = habit.copyWith(
        isDoneToday: isDone,
        lastCompletedAt: isDone ? DateTime.now() : null,
      );
    });
  }

  void deleteHabit(int index) {
    setState(() {
      habits.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Habit Tracker'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Hari ini',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 8),
            Text('$completedCount dari ${habits.length} habit selesai'),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: habitController,
                    decoration: const InputDecoration(
                      labelText: 'Habit baru',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => addHabit(),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: addHabit,
                  child: const Text('Tambah'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Expanded(
              child: habits.isEmpty
                  ? const Center(child: Text('Belum ada habit'))
                  : ListView.builder(
                      itemCount: habits.length,
                      itemBuilder: (context, index) {
                        final habit = habits[index];

                        return ListTile(
                          leading: Checkbox(
                            value: habit.isDoneToday,
                            onChanged: (_) => toggleHabit(index),
                          ),
                          title: Text(
                            habit.title,
                            style: TextStyle(
                              decoration: habit.isDoneToday
                                  ? TextDecoration.lineThrough
                                  : TextDecoration.none,
                            ),
                          ),
                          trailing: IconButton(
                            icon: const Icon(Icons.delete),
                            onPressed: () => deleteHabit(index),
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

Ini belum menyimpan data. Kalau aplikasi ditutup, habit akan hilang. Itu normal untuk tahap ini.


15. Upgrade Model Habit untuk JSON

Agar bisa disimpan ke local storage, model perlu toJson dan fromJson.

Update habit.dart:

class Habit {
  final String id;
  final String title;
  final bool isDoneToday;
  final DateTime createdAt;
  final DateTime? lastCompletedAt;

  const Habit({
    required this.id,
    required this.title,
    required this.isDoneToday,
    required this.createdAt,
    this.lastCompletedAt,
  });

  factory Habit.fromJson(Map<String, dynamic> json) {
    return Habit(
      id: json['id'],
      title: json['title'],
      isDoneToday: json['isDoneToday'] ?? false,
      createdAt: DateTime.parse(json['createdAt']),
      lastCompletedAt: json['lastCompletedAt'] == null
          ? null
          : DateTime.parse(json['lastCompletedAt']),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'isDoneToday': isDoneToday,
      'createdAt': createdAt.toIso8601String(),
      'lastCompletedAt': lastCompletedAt?.toIso8601String(),
    };
  }

  Habit copyWith({
    String? id,
    String? title,
    bool? isDoneToday,
    DateTime? createdAt,
    DateTime? lastCompletedAt,
  }) {
    return Habit(
      id: id ?? this.id,
      title: title ?? this.title,
      isDoneToday: isDoneToday ?? this.isDoneToday,
      createdAt: createdAt ?? this.createdAt,
      lastCompletedAt: lastCompletedAt ?? this.lastCompletedAt,
    );
  }
}

Penjelasan:

  • toJson mengubah object Habit menjadi Map
  • fromJson mengubah Map menjadi object Habit
  • DateTime disimpan sebagai string ISO
  • lastCompletedAt boleh null

Kenapa tanggal diubah ke string?

Karena local storage lebih mudah menyimpan string daripada object DateTime.


16. Install Shared Preferences

Command:

flutter pub add shared_preferences

Package ini dipakai untuk menyimpan data sederhana di device.

Untuk Habit Tracker versi awal, shared_preferences sudah cukup.


17. Membuat HabitStorageService

File:

lib/services/habit_storage_service.dart

Isi:

import 'dart:convert';

import 'package:shared_preferences/shared_preferences.dart';

import '../models/habit.dart';

class HabitStorageService {
  static const String _habitsKey = 'habits';

  Future<List<Habit>> loadHabits() async {
    final prefs = await SharedPreferences.getInstance();
    final habitsString = prefs.getString(_habitsKey);

    if (habitsString == null) {
      return [];
    }

    final List<dynamic> decodedList = jsonDecode(habitsString);

    return decodedList
        .map((item) => Habit.fromJson(item as Map<String, dynamic>))
        .toList();
  }

  Future<void> saveHabits(List<Habit> habits) async {
    final prefs = await SharedPreferences.getInstance();
    final encodedList = habits.map((habit) => habit.toJson()).toList();
    final habitsString = jsonEncode(encodedList);

    await prefs.setString(_habitsKey, habitsString);
  }
}

Penjelasan:

  • _habitsKey adalah nama key penyimpanan
  • loadHabits membaca string JSON dari storage
  • jika belum ada data, return list kosong
  • jsonDecode mengubah string menjadi list
  • Habit.fromJson mengubah item JSON menjadi object
  • saveHabits mengubah list habit menjadi JSON string
  • prefs.setString menyimpan data

18. Prompt untuk Local Storage

Prompt:

Saya sudah punya model Habit dan HabitPage.
Bantu tambahkan local storage dengan shared_preferences.

Tolong buat:
- toJson dan fromJson di Habit
- HabitStorageService
- loadHabits saat halaman dibuka
- saveHabits setelah add, toggle, delete
- loading state saat data dibaca
- jelaskan alur datanya

Minta AI membuat satu tahap dulu:

Mulai dari HabitStorageService saja.
Setelah itu tunggu saya jalankan.

19. Integrasi Storage ke HabitPage

Tambahkan service:

final HabitStorageService storageService = HabitStorageService();

Ubah list agar bisa diisi dari storage:

List<Habit> habits = [];
bool isLoading = true;

Tambahkan initState:

@override
void initState() {
  super.initState();
  loadHabits();
}

Tambahkan function load:

Future<void> loadHabits() async {
  final loadedHabits = await storageService.loadHabits();

  setState(() {
    habits = loadedHabits;
    isLoading = false;
  });
}

Tambahkan function save:

Future<void> saveHabits() async {
  await storageService.saveHabits(habits);
}

Setelah add habit:

setState(() {
  habits.add(habit);
  habitController.clear();
});

saveHabits();

Setelah toggle:

setState(() {
  habits[index] = habit.copyWith(
    isDoneToday: isDone,
    lastCompletedAt: isDone ? DateTime.now() : null,
  );
});

saveHabits();

Setelah delete:

setState(() {
  habits.removeAt(index);
});

saveHabits();

Saat loading:

if (isLoading) {
  return const Scaffold(
    body: Center(
      child: CircularProgressIndicator(),
    ),
  );
}

20. Reset Status Harian

Habit Tracker seharusnya menandai habit selesai untuk hari tertentu.

Untuk versi sederhana, kita bisa reset isDoneToday jika lastCompletedAt bukan hari ini.

Tambahkan helper:

bool isSameDay(DateTime a, DateTime b) {
  return a.year == b.year && a.month == b.month && a.day == b.day;
}

Saat load data:

Future<void> loadHabits() async {
  final loadedHabits = await storageService.loadHabits();
  final today = DateTime.now();

  final normalizedHabits = loadedHabits.map((habit) {
    final lastCompletedAt = habit.lastCompletedAt;

    if (lastCompletedAt == null) {
      return habit.copyWith(isDoneToday: false);
    }

    final doneToday = isSameDay(lastCompletedAt, today);

    return habit.copyWith(isDoneToday: doneToday);
  }).toList();

  setState(() {
    habits = normalizedHabits;
    isLoading = false;
  });

  await storageService.saveHabits(normalizedHabits);
}

Penjelasan:

  • app mengecek tanggal terakhir habit selesai
  • jika tanggalnya bukan hari ini, status dikembalikan ke false
  • ini membuat habit bisa dipakai lagi setiap hari

21. Menampilkan Progress

Tambahkan progress percent:

double get progress {
  if (habits.isEmpty) return 0;
  return completedCount / habits.length;
}

Tambahkan UI:

LinearProgressIndicator(value: progress),
const SizedBox(height: 8),
Text('${(progress * 100).round()}% selesai'),

Penjelasan:

  • jika list kosong, progress 0
  • jika 3 dari 5 habit selesai, progress 0.6
  • LinearProgressIndicator menerima nilai 0 sampai 1

22. Full Flow Aplikasi

Alur lengkap:

App dibuka
  -> initState
  -> loadHabits dari SharedPreferences
  -> reset status jika bukan hari ini
  -> tampilkan list

User tambah habit
  -> ambil text dari controller
  -> validasi kosong
  -> buat object Habit
  -> masukkan ke list
  -> saveHabits

User centang habit
  -> toggle isDoneToday
  -> isi lastCompletedAt
  -> saveHabits

User hapus habit
  -> remove dari list
  -> saveHabits

Kalau kamu bisa menjelaskan flow ini, berarti pemahaman project sudah mulai kuat.


23. Prompt Review Kode

Setelah fitur berjalan, minta AI review:

Tolong review kode Habit Tracker saya.

Fokus review:
- apakah struktur file sudah rapi
- apakah state terlalu banyak di UI
- apakah local storage sudah aman untuk pemula
- apakah ada bug pada reset harian
- apakah ada memory leak
- apakah error handling perlu ditambah
- apa refactor kecil yang disarankan

Jangan ubah kode dulu.
Berikan review dan prioritas perbaikan.

Prompt ini membantu kamu belajar membaca kualitas kode.


24. Error yang Sering Muncul

Data tidak tersimpan

Penyebab:

  • lupa memanggil saveHabits
  • toJson salah
  • key storage berbeda antara save dan load

Solusi:

  • cek _habitsKey
  • cek saveHabits dipanggil setelah add/toggle/delete
  • print string JSON untuk debugging

Error DateTime.parse

Penyebab:

  • value tanggal null
  • format tanggal tidak valid

Solusi:

  • pastikan createdAt selalu ada
  • cek lastCompletedAt boleh null

UI tidak update

Penyebab:

  • lupa setState
  • perubahan list dilakukan di luar update UI

Solusi:

  • pastikan perubahan list dibungkus setState

Checkbox kembali false

Penyebab:

  • reset harian menganggap tanggal bukan hari ini
  • timezone atau data lama bermasalah

Solusi:

  • cek lastCompletedAt
  • cek function isSameDay

25. Checklist Testing Manual

Gunakan checklist ini:

  • aplikasi bisa dibuka
  • empty state muncul saat belum ada habit
  • user bisa tambah habit
  • input kosong tidak menambah data
  • setelah tambah, input kosong kembali
  • habit muncul di list
  • checkbox bisa dicentang
  • progress berubah setelah checkbox dicentang
  • habit bisa dihapus
  • data tetap ada setelah app ditutup
  • status selesai hari ini tetap tersimpan
  • status reset saat hari berbeda
  • tidak ada overflow di layar kecil
  • loading muncul saat data dibaca

Testing tambahan:

  • tambah 10 habit
  • hapus habit pertama
  • hapus habit terakhir
  • centang semua habit
  • hapus semua habit
  • tutup dan buka aplikasi lagi

26. Catatan Belajar Setelah Selesai

Simpan catatan:

# Project Habit Tracker

## Fitur
- tambah habit
- list habit
- toggle selesai hari ini
- hapus habit
- progress harian
- local storage

## Konsep Flutter
- StatefulWidget
- setState
- TextEditingController
- ListView
- Checkbox
- LinearProgressIndicator

## Konsep Dart
- class
- List
- DateTime
- toJson
- fromJson
- copyWith

## Error yang Dipelajari
- lupa setState membuat UI tidak update
- lupa save membuat data hilang
- DateTime perlu diubah ke string saat disimpan

## Next
- tambah fitur edit habit
- tambah kategori habit
- refactor ke Provider

Dokumentasi ini akan berguna saat kamu lupa atau ingin membuat project serupa.


27. Challenge Pengembangan

Setelah MVP selesai, coba tambah fitur:

Challenge 1: Edit Habit

Fitur:

  • tap habit
  • buka dialog edit
  • ubah title
  • simpan perubahan

Prompt:

Tambahkan fitur edit habit ke Habit Tracker saya.
Gunakan AlertDialog dengan TextField.
Setelah simpan, update list dan save ke local storage.
Jelaskan perubahan kode.

Challenge 2: Kategori Habit

Fitur:

  • habit punya kategori
  • contoh kategori: Health, Study, Work
  • tampilkan kategori di subtitle

Challenge 3: Filter Habit

Fitur:

  • semua
  • selesai
  • belum selesai

Challenge 4: Statistik Mingguan

Fitur:

  • simpan history tanggal selesai
  • tampilkan jumlah selesai 7 hari terakhir

Untuk challenge 4, model perlu dikembangkan lagi.


28. Refactor ke Provider

Setelah versi StatefulWidget paham, lanjut refactor ke Provider.

Target struktur:

lib/
  models/
    habit.dart
  services/
    habit_storage_service.dart
  providers/
    habit_provider.dart
  pages/
    habit_page.dart

Prompt:

Saya sudah punya Habit Tracker dengan StatefulWidget.
Bantu refactor ke Provider.

Target:
- HabitProvider menyimpan list habit
- HabitProvider punya loadHabits, addHabit, toggleHabit, deleteHabit
- HabitPage hanya fokus UI
- HabitStorageService tetap dipakai
- jelaskan alur UI -> Provider -> Storage

Refactor ini penting agar project lebih siap berkembang.


29. Upgrade ke Firebase

Setelah local storage aman, project bisa di-upgrade ke Firebase.

Target:

  • user login
  • habit tersimpan di Firestore
  • habit punya userId
  • data per user
  • realtime update
  • security rules

Prompt:

Saya punya Habit Tracker lokal dengan Provider dan local storage.
Bantu buat roadmap upgrade ke Firebase.

Target:
- Firebase Auth
- Firestore collection habits
- HabitRepository
- data per user
- Security Rules
- migrasi bertahap tanpa langsung menghapus versi local storage

Untuk tahap awal, jangan langsung membuat sync offline-online. Mulai dari Firestore online dulu.


30. Kesimpulan

Project Habit Tracker adalah latihan bagus untuk belajar Flutter Mobile dengan vibe coding.

Yang dipelajari:

  • membuat project dari ide
  • memecah fitur menjadi tahap kecil
  • membuat prompt yang jelas
  • membangun model data
  • membuat UI list
  • mengelola state lokal
  • menyimpan data dengan local storage
  • membaca error
  • melakukan testing manual
  • membuat dokumentasi belajar

Pola penting:

Prompt kecil
-> kode kecil
-> run
-> debug
-> pahami
-> catat
-> lanjut

Dengan pola ini, vibe coding menjadi cara belajar yang produktif, bukan sekadar copy-paste.


31. Selanjutnya

Materi berikutnya yang cocok adalah Project Flutter Mobile Vibe Coding: Expense Tracker.

Di project itu kita akan belajar data transaksi, kategori, total saldo, filter bulan, dan chart sederhana.