Lompat ke konten utama
←

Flutter Dasar: Dart, Widget, dan State

Pelajaran 14 dari 18

State, navigasi, dan formulir

Misi: aplikasi daftar tugas

Setelah pelajaran ini kamu bisa menelusuri aplikasi daftar tugas yang memadukan Form dan validator, setState, ListView, dan callback dari widget anak ke induk.

  • ±17 menit
  • 4 latihan
  • Diperiksa 28 Okt 2026

Latihan di pelajaran ini butuh akun gratis. Progres, kode, dan catatanmu hanya tersimpan di browser ini. Masuk untuk menyimpannya

Materi

Pengurus koperasi karyawan di Balikpapan memakai aplikasi daftar tugas sederhana, dan programnya ada di bawah. _DaftarTugasState memegang daftar tugas, formulir di atas menambah tugas baru, dan setiap baris berupa BarisTugas yang stateless. Bacalah dengan urutan ini: data apa yang disimpan State, kapan setState dipanggil, dan ke mana callback dari BarisTugas mengarah.

class Tugas {
  final String judul;
  final bool selesai;

  const Tugas(this.judul, {this.selesai = false});

  Tugas tandai(bool nilai) => Tugas(judul, selesai: nilai);
}

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

  @override
  State<DaftarTugas> createState() => _DaftarTugasState();
}

class _DaftarTugasState extends State<DaftarTugas> {
  final _formKey = GlobalKey<FormState>();
  final _judul = TextEditingController();
  final List<Tugas> _tugas = [];

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

  void _tambah() {
    if (!_formKey.currentState!.validate()) return;
    setState(() {
      _tugas.add(Tugas(_judul.text.trim()));
    });
    _judul.clear();
  }

  @override
  Widget build(BuildContext context) {
    final sisa = _tugas.where((t) => !t.selesai).length;
    return Scaffold(
      appBar: AppBar(title: Text('Tugas ($sisa belum selesai)')),
      body: Column(
        children: [
          Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _judul,
                    validator: (value) {
                      if (value == null || value.trim().isEmpty) {
                        return 'Judul wajib diisi';
                      }
                      if (value.trim().length > 40) return 'Maksimal 40 huruf';
                      return null;
                    },
                  ),
                ),
                IconButton(icon: const Icon(Icons.add), onPressed: _tambah),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tugas.length,
              itemBuilder: (context, i) => BarisTugas(
                tugas: _tugas[i],
                onUbah: (nilai) => setState(() => _tugas[i] = _tugas[i].tandai(nilai)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BarisTugas extends StatelessWidget {
  const BarisTugas({super.key, required this.tugas, required this.onUbah});

  final Tugas tugas;
  final ValueChanged<bool> onUbah;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(tugas.judul),
      value: tugas.selesai,
      onChanged: (v) => onUbah(v ?? false),
    );
  }
}

Empat soal berikut memakai program yang sama. Dua di antaranya mengubah satu bagian; bayangkan perubahan itu pada salinan program, bukan pada aslinya. Judul AppBar dihitung di build dari isi _tugas, jadi judul itu hanya berubah bila build milik _DaftarTugasState dijalankan ulang.

Sebelum menjawab, tandai tiga tempat di program: _tambah yang memvalidasi lalu menambah tugas di dalam setState, onUbah yang mengganti tugas di dalam setState, dan validator yang memeriksa judul setelah trim(). Hampir setiap soal bergantung pada salah satu dari ketiganya.

Latihan

0/4 lulus

Misi akhir modul: luluskan minimal 4 dari 4 tugas. Jawaban diperiksa di server. Lulus berarti kamu memahami contohnya, bukan penilaian seluruh pekerjaanmu.

Latihan 1 dari 4

Pengguna mengetik tiga spasi di kolom judul lalu mengetuk tombol plus. Apa yang terjadi?

class Tugas {
  final String judul;
  final bool selesai;

  const Tugas(this.judul, {this.selesai = false});

  Tugas tandai(bool nilai) => Tugas(judul, selesai: nilai);
}

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

  @override
  State<DaftarTugas> createState() => _DaftarTugasState();
}

class _DaftarTugasState extends State<DaftarTugas> {
  final _formKey = GlobalKey<FormState>();
  final _judul = TextEditingController();
  final List<Tugas> _tugas = [];

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

  void _tambah() {
    if (!_formKey.currentState!.validate()) return;
    setState(() {
      _tugas.add(Tugas(_judul.text.trim()));
    });
    _judul.clear();
  }

  @override
  Widget build(BuildContext context) {
    final sisa = _tugas.where((t) => !t.selesai).length;
    return Scaffold(
      appBar: AppBar(title: Text('Tugas ($sisa belum selesai)')),
      body: Column(
        children: [
          Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _judul,
                    validator: (value) {
                      if (value == null || value.trim().isEmpty) {
                        return 'Judul wajib diisi';
                      }
                      if (value.trim().length > 40) return 'Maksimal 40 huruf';
                      return null;
                    },
                  ),
                ),
                IconButton(icon: const Icon(Icons.add), onPressed: _tambah),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tugas.length,
              itemBuilder: (context, i) => BarisTugas(
                tugas: _tugas[i],
                onUbah: (nilai) => setState(() => _tugas[i] = _tugas[i].tandai(nilai)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BarisTugas extends StatelessWidget {
  const BarisTugas({super.key, required this.tugas, required this.onUbah});

  final Tugas tugas;
  final ValueChanged<bool> onUbah;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(tugas.judul),
      value: tugas.selesai,
      onChanged: (v) => onUbah(v ?? false),
    );
  }
}
Pilih satu jawaban

Latihan 2 dari 4

Pengguna menambahkan Setor kas harian, Beli kertas struk, dan Rekap absen, lalu mencentang Beli kertas struk. Apa judul AppBar sekarang?

class Tugas {
  final String judul;
  final bool selesai;

  const Tugas(this.judul, {this.selesai = false});

  Tugas tandai(bool nilai) => Tugas(judul, selesai: nilai);
}

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

  @override
  State<DaftarTugas> createState() => _DaftarTugasState();
}

class _DaftarTugasState extends State<DaftarTugas> {
  final _formKey = GlobalKey<FormState>();
  final _judul = TextEditingController();
  final List<Tugas> _tugas = [];

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

  void _tambah() {
    if (!_formKey.currentState!.validate()) return;
    setState(() {
      _tugas.add(Tugas(_judul.text.trim()));
    });
    _judul.clear();
  }

  @override
  Widget build(BuildContext context) {
    final sisa = _tugas.where((t) => !t.selesai).length;
    return Scaffold(
      appBar: AppBar(title: Text('Tugas ($sisa belum selesai)')),
      body: Column(
        children: [
          Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _judul,
                    validator: (value) {
                      if (value == null || value.trim().isEmpty) {
                        return 'Judul wajib diisi';
                      }
                      if (value.trim().length > 40) return 'Maksimal 40 huruf';
                      return null;
                    },
                  ),
                ),
                IconButton(icon: const Icon(Icons.add), onPressed: _tambah),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tugas.length,
              itemBuilder: (context, i) => BarisTugas(
                tugas: _tugas[i],
                onUbah: (nilai) => setState(() => _tugas[i] = _tugas[i].tandai(nilai)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BarisTugas extends StatelessWidget {
  const BarisTugas({super.key, required this.tugas, required this.onUbah});

  final Tugas tugas;
  final ValueChanged<bool> onUbah;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(tugas.judul),
      value: tugas.selesai,
      onChanged: (v) => onUbah(v ?? false),
    );
  }
}
Pilih satu jawaban

Latihan 3 dari 4

Pada salinan program, setState di _tambah dihapus sehingga isinya hanya _tugas.add(...). Pengguna menambah Bayar listrik ke daftar yang masih kosong. Apa yang terlihat?

class Tugas {
  final String judul;
  final bool selesai;

  const Tugas(this.judul, {this.selesai = false});

  Tugas tandai(bool nilai) => Tugas(judul, selesai: nilai);
}

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

  @override
  State<DaftarTugas> createState() => _DaftarTugasState();
}

class _DaftarTugasState extends State<DaftarTugas> {
  final _formKey = GlobalKey<FormState>();
  final _judul = TextEditingController();
  final List<Tugas> _tugas = [];

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

  void _tambah() {
    if (!_formKey.currentState!.validate()) return;
    setState(() {
      _tugas.add(Tugas(_judul.text.trim()));
    });
    _judul.clear();
  }

  @override
  Widget build(BuildContext context) {
    final sisa = _tugas.where((t) => !t.selesai).length;
    return Scaffold(
      appBar: AppBar(title: Text('Tugas ($sisa belum selesai)')),
      body: Column(
        children: [
          Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _judul,
                    validator: (value) {
                      if (value == null || value.trim().isEmpty) {
                        return 'Judul wajib diisi';
                      }
                      if (value.trim().length > 40) return 'Maksimal 40 huruf';
                      return null;
                    },
                  ),
                ),
                IconButton(icon: const Icon(Icons.add), onPressed: _tambah),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tugas.length,
              itemBuilder: (context, i) => BarisTugas(
                tugas: _tugas[i],
                onUbah: (nilai) => setState(() => _tugas[i] = _tugas[i].tandai(nilai)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BarisTugas extends StatelessWidget {
  const BarisTugas({super.key, required this.tugas, required this.onUbah});

  final Tugas tugas;
  final ValueChanged<bool> onUbah;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(tugas.judul),
      value: tugas.selesai,
      onChanged: (v) => onUbah(v ?? false),
    );
  }
}
Pilih satu jawaban

Latihan 4 dari 4

Kenapa judul AppBar ikut berubah saat kotak centang di BarisTugas diketuk, padahal BarisTugas stateless?

class Tugas {
  final String judul;
  final bool selesai;

  const Tugas(this.judul, {this.selesai = false});

  Tugas tandai(bool nilai) => Tugas(judul, selesai: nilai);
}

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

  @override
  State<DaftarTugas> createState() => _DaftarTugasState();
}

class _DaftarTugasState extends State<DaftarTugas> {
  final _formKey = GlobalKey<FormState>();
  final _judul = TextEditingController();
  final List<Tugas> _tugas = [];

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

  void _tambah() {
    if (!_formKey.currentState!.validate()) return;
    setState(() {
      _tugas.add(Tugas(_judul.text.trim()));
    });
    _judul.clear();
  }

  @override
  Widget build(BuildContext context) {
    final sisa = _tugas.where((t) => !t.selesai).length;
    return Scaffold(
      appBar: AppBar(title: Text('Tugas ($sisa belum selesai)')),
      body: Column(
        children: [
          Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _judul,
                    validator: (value) {
                      if (value == null || value.trim().isEmpty) {
                        return 'Judul wajib diisi';
                      }
                      if (value.trim().length > 40) return 'Maksimal 40 huruf';
                      return null;
                    },
                  ),
                ),
                IconButton(icon: const Icon(Icons.add), onPressed: _tambah),
              ],
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _tugas.length,
              itemBuilder: (context, i) => BarisTugas(
                tugas: _tugas[i],
                onUbah: (nilai) => setState(() => _tugas[i] = _tugas[i].tandai(nilai)),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BarisTugas extends StatelessWidget {
  const BarisTugas({super.key, required this.tugas, required this.onUbah});

  final Tugas tugas;
  final ValueChanged<bool> onUbah;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text(tugas.judul),
      value: tugas.selesai,
      onChanged: (v) => onUbah(v ?? false),
    );
  }
}
Pilih satu jawaban

Catatanmu

Memuat catatan…

tersimpan otomatis, ikut tercetak · 0/4.000

Lanjutkan ke