Meneruskan data

Mengirim data ke layar berikutnya dan mengembalikan hasil ke layar sebelumnya.

Daftar isi

Perpindahan layar pada aplikasi nyata hampir selalu membawa data, misalnya layar detail catatan membutuhkan catatan yang dipilih dan layar daftar menunggu catatan baru dari formulir. Topik ini membahas aliran data dua arah antarlayar pada Flutter, yaitu mengirim data ke layar berikutnya melalui konstruktor dan mengembalikan hasil ke layar sebelumnya melalui Navigator.pop yang ditunggu dengan await. Kasus yang digunakan sepanjang topik adalah alur tambah catatan pada aplikasi catatan.

Mengirim data ke layar berikutnya#

Cara paling langsung untuk mengirim data ke layar berikutnya adalah melalui konstruktor layar tujuan. Konstruktor mendeklarasikan data yang wajib diterima dengan required, sehingga kode pemanggil yang lupa mengirim data ditolak oleh pemeriksa tipe sebelum aplikasi berjalan. Kontrak antarlayar dengan demikian terbaca langsung pada deklarasi konstruktornya.

dart Contoh: layar detail menerima id melalui konstruktor
// tidak-dijalankan
class LayarDetailCatatan extends StatelessWidget {
  const LayarDetailCatatan({super.key, required this.id});

  final int id;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Catatan $id')),
      body: Text('Isi catatan dengan id $id.'),
    );
  }
}

Pembukaan layar menyertakan data pada saat yang sama dengan pembuatan route. Nilai yang diteruskan tetap bertipe int tanpa cast, dan kesalahan tipe terdeteksi saat kompilasi. Keunggulan inilah yang tidak dimiliki argumen bertipe Object? pada route bernama.

Data yang diteruskan tidak terbatas pada bilangan dan teks. Objek seperti Catatan diteruskan dengan cara yang sama, cukup dengan mengganti tipe parameternya, sehingga layar detail dapat menerima catatan utuh alih-alih id yang harus dicari ulang. Pola yang sama juga digunakan untuk mengirim nilai awal formulir, misalnya pada layar penyuntingan yang perlu mengisi field dengan isi lama.

dart Contoh: membuka layar detail sambil meneruskan id
// tidak-dijalankan
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const LayarDetailCatatan(id: 3),
  ),
);

Mengembalikan hasil dengan pop dan await#

Arah sebaliknya memanfaatkan sifat Navigator.push yang mengembalikan Future<T?>. Future tersebut selesai dengan nilai yang dikirim Navigator.pop dari layar yang ditutup, atau selesai dengan null apabila layar ditutup tanpa mengirim nilai. Pemanggilan Navigator.push sering dilakukan tanpa menunggu hasilnya. Dengan menunggu Future tersebut menggunakan await, kode layar pemanggil terbaca seperti alur sinkron.

dart Contoh: pop mengirim kembali catatan baru
// tidak-dijalankan
Navigator.pop(context, Catatan(judul: judul, isi: isi));

Parameter kedua Navigator.pop adalah nilai yang dikembalikan, sedangkan parameter generik pada Navigator.push<Catatan> menetapkan tipe hasil Future tersebut. Kombinasi keduanya memberi kode pemanggil variabel hasil bertipe Catatan? tanpa cast. Tanda tanya pada tipe tersebut menegaskan bahwa hasil bisa berupa null dan wajib diperiksa sebelum digunakan.

Nilai kembalian dari satu pop hanya satu, sehingga hasil yang terdiri atas beberapa bagian sebaiknya dibungkus dalam satu kelas seperti Catatan. Meneruskan banyak nilai secara terpisah tidak mungkin dilakukan melalui satu pop, dan membungkusnya dalam satu kelas lebih aman daripada meneruskan nilai-nilai terpisah yang urutannya mudah tertukar.

Alur tambah catatan utuh#

Contoh berikut merangkai kedua arah menjadi satu alur, yaitu layar daftar membuka layar tambah, layar tambah mengirim kembali catatan baru, dan layar daftar memasukkannya melalui setState. Kelas Catatan menjadi kontrak data yang digunakan bersama oleh kedua layar.

dart Contoh: kelas Catatan yang diteruskan antarlayar
// tidak-dijalankan
class Catatan {
  const Catatan({required this.judul, required this.isi});

  final String judul;
  final String isi;
}

Layar tambah menyimpan teks dari dua TextField pada dua TextEditingController, lalu mengirimkannya kembali melalui Navigator.pop. Controller dibuang melalui dispose agar sumber daya yang digunakannya terlepas setelah layar ditutup. Validasi sederhana berupa penolakan judul kosong cukup menjaga contoh tetap ringkas.

dart Contoh: layar tambah catatan yang mengembalikan hasil
// tidak-dijalankan
class LayarTambahCatatan extends StatefulWidget {
  const LayarTambahCatatan({super.key});

  @override
  State<LayarTambahCatatan> createState() => _LayarTambahCatatanState();
}

class _LayarTambahCatatanState extends State<LayarTambahCatatan> {
  final _pengendaliJudul = TextEditingController();
  final _pengendaliIsi = TextEditingController();

  @override
  void dispose() {
    _pengendaliJudul.dispose();
    _pengendaliIsi.dispose();
    super.dispose();
  }

  void _simpan() {
    if (_pengendaliJudul.text.isEmpty) return;
    Navigator.pop(
      context,
      Catatan(judul: _pengendaliJudul.text, isi: _pengendaliIsi.text),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Tambah catatan')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _pengendaliJudul,
              decoration: const InputDecoration(labelText: 'Judul'),
            ),
            TextField(
              controller: _pengendaliIsi,
              decoration: const InputDecoration(labelText: 'Isi catatan'),
            ),
            const SizedBox(height: 16),
            FilledButton(onPressed: _simpan, child: const Text('Simpan')),
          ],
        ),
      ),
    );
  }
}

Di sisi pemanggil, hasil await diperiksa terlebih dahulu, kemudian catatan baru ditambahkan ke daftar melalui setState agar antarmuka diperbarui. Variabel daftarCatatan pada contoh adalah daftar milik state layar pertama, sehingga setState cukup menambahkan satu elemen dan perenderan ulang yang menyusul menyesuaikan tampilan daftar secara otomatis. Pola ini melengkapi kedua arah penerusan data, yaitu data turun ke layar tujuan melalui konstruktor dan hasil naik kembali melalui nilai pop.

dart Contoh: layar daftar menunggu catatan baru dengan await
// tidak-dijalankan
final hasil = await Navigator.push<Catatan>(
  context,
  MaterialPageRoute(builder: (context) => const LayarTambahCatatan()),
);
if (hasil != null && mounted) {
  setState(() {
    daftarCatatan.add(hasil);
  });
}

Apabila hasil berupa null, blok if dilewati dan daftar tetap seperti semula, sebab tidak ada catatan yang dikirim dari layar tambah. Kondisi mounted pada pemeriksaan yang sama menjaga agar setState tidak dipanggil ketika layar daftar sudah tidak aktif.

Perhatikan bahwa layar tambah tidak menyimpan catatan ke mana pun. Tugasnya berhenti pada mengirim objek Catatan melalui Navigator.pop, sedangkan penyimpanannya menjadi keputusan layar pemanggil. Pembagian tanggung jawab ini menjaga layar tambah tetap dapat digunakan ulang, misalnya untuk formulir dengan kebutuhan tampilan yang berbeda.

Kesalahan umum#

Kesalahan pertama adalah menyentuh context setelah await tanpa memeriksa mounted. Apabila layar sudah tidak aktif ketika Future selesai, pemanggilan setState atau Navigator dengan context lama memunculkan error. Perbaikannya berupa pengujian mounted sebelum kode pasca-await menyentuh context atau state.

Kesalahan kedua adalah menganggap hasil push selalu berisi nilai. Penekanan tombol kembali bawaan sistem menghasilkan null, dan kode yang langsung membaca isi hasil akan memunculkan error. Pemeriksaan if (hasil != null) pada contoh di atas dirancang untuk kasus ini.

Kesalahan ketiga berupa kelalaian menetapkan parameter generik pada push. Tanpa Navigator.push<Catatan>, hasil yang diterima bertipe dynamic, sehingga kekeliruan tipe tidak lagi tertangkap sebelum aplikasi berjalan. Kebiasaan menulis tipe generiknya mempertahankan keunggulan pemeriksaan tipe sampai ujung alur.

Ringkasan#

  • Data dikirim ke layar berikutnya melalui parameter konstruktor yang bertipe kuat dan wajib diisi.
  • Navigator.pop menerima nilai kedua yang dikembalikan ke pemanggil, sedangkan Navigator.push mengembalikannya sebagai Future<T?> yang ditunggu dengan await.
  • Hasil null tetap mungkin terjadi karena tombol kembali bawaan sistem, sehingga nilai kembalian wajib diperiksa sebelum digunakan.
  • Kelas data sederhana seperti Catatan menjadi kontrak bersama kedua arah aliran.

Referensi#