FutureBuilder

Merender hasil Future dengan FutureBuilder untuk kondisi loading, error, dan kosong.

Daftar isi

Setelah permintaan data dikirim, layar harus menampilkan sesuatu selama data belum tiba, lalu berubah sesuai hasilnya, baik data maupun error. Topik ini membahas FutureBuilder, widget yang menangani pergantian keadaan tersebut secara deklaratif melalui satu fungsi builder. Percabangan keadaan memuat, error, data, dan daftar kosong dibahas lengkap beserta diagram alurnya, dan hasilnya disambungkan ke layar detail sebagaimana pola pada pembahasan navigasi.

Keadaan yang harus ditangani layar data#

Saat data diambil dari API, layar tidak boleh berpindah ke keadaan berikutnya sebelum waktunya. Indikator memuat harus tampil selama permintaan berjalan, pesan error harus tampil apabila permintaan gagal, dan isi data baru boleh dirender setelah benar-benar tersedia. Tanpa bantuan widget, pola ini ditulis manual, yaitu memanggil metode asinkron pada initState, menandai keadaan lewat variabel, lalu memanggil setState setiap kali keadaan berpindah. Pola yang sama akan diulang di setiap layar yang mengambil data, dan bagian yang terlewat, misalnya penanganan errornya, baru tampak ketika jaringan bermasalah.

FutureBuilder memindahkan pola itu ke dalam widget. Properti future diisi Future yang ditunggu, sedangkan properti builder adalah fungsi yang dipanggil ulang setiap kali keadaan Future berpindah. Fungsi builder menerima BuildContext dan AsyncSnapshot, yaitu objek yang membawa keadaan saat ini beserta data atau error yang sudah tersedia.

Keadaan-keadaan itu terjadi bergantian, dan satu layar hanya berada pada satu keadaan pada satu waktu. Sifat inilah yang membuat tanggung jawab layar dapat dipecah menjadi percabangan berurutan di dalam satu fungsi, tanpa variabel keadaan tambahan yang harus dijaga sinkronnya.

ConnectionState dan AsyncSnapshot#

Enum ConnectionState menyatakan keadaan sambungan dengan pekerjaan asinkron dan memiliki empat nilai. Tabel berikut merangkum arti tiap nilai pada konteks Future.

NilaiArti
ConnectionState.nonebelum tersambung dengan pekerjaan asinkron apa pun
ConnectionState.waitingpekerjaan sedang berjalan dan hasil belum tiba
ConnectionState.activepekerjaan masih aktif, lazim pada Stream
ConnectionState.donepekerjaan selesai, hasil atau error sudah tersedia

Untuk Future tunggal, keadaan yang benar-benar dialami adalah waiting lalu done, sedangkan active menjadi relevan pada Stream yang datanya bertahap. AsyncSnapshot menyediakan properti untuk membaca keadaan tersebut, yaitu connectionState untuk enum di atas, hasError beserta error untuk kegagalan, dan hasData beserta data untuk hasil. Diagram berikut merangkum percabangan yang lazim ditulis di dalam builder.

waitingindikator memuatdonedone + datatampilkan daftardone + errortampilkan pesan errorhasDatahasError
Percabangan ConnectionState pada builder FutureBuilder

Percabangan pada diagram bukan daftar pilihan yang boleh diambil sebagian. Indikator memuat menjaga agar pengguna tidak menatap layar kosong, cabang error mencegah kegagalan tampil sebagai layar kosong, dan cabang data hanya membaca snapshot.data setelah hasilnya benar-benar ada. Ketiganya bersama-sama membentuk tanggung jawab minimum layar yang mengambil data.

Kerangka lengkap dalam builder#

dart Contoh: percabangan lengkap pada builder FutureBuilder
// tidak-dijalankan
FutureBuilder<List<Catatan>>(
  future: _futureCatatan,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    if (snapshot.hasError) {
      return Center(child: Text('Gagal memuat catatan: ${snapshot.error}'));
    }
    final daftar = snapshot.data ?? const <Catatan>[];
    if (daftar.isEmpty) {
      return const Center(child: Text('Belum ada catatan'));
    }
    return ListView.builder(
      itemCount: daftar.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(daftar[index].judul));
      },
    );
  },
)

Urutan pemeriksaan pada kerangka di atas sengaja dibuat berlapis. Pemeriksaan waiting ditulis paling awal agar keadaan memuat langsung tampil sebagai indikator, bukan layar kosong. Pemeriksaan hasError berikutnya menangkap Future yang selesai dengan kegagalan, dan snapshot.error ditampilkan sehingga penyebabnya terbaca. Baru setelah keduanya lolos, data dibaca melalui snapshot.data, dan ?? const <Catatan>[] memberi daftar kosong apabila data ternyata null.

Kata kunci const pada cabang-cabang awal membuat widget yang tetap, seperti indikator memuat, tidak ikut dibangun ulang pada setiap pemanggilan builder. Pada cabang data, ListView.builder dari pembahasan list digunakan sebagaimana adanya, karena data yang baru tiba memang paling sering berbentuk list.

Parameter generik pada FutureBuilder<List<Catatan>> menyatakan tipe data yang ditunggu, sehingga snapshot.data bertipe List<Catatan>? tanpa perlu cast lagi di dalam builder. Tipe generik ini wajib sejalan dengan tipe yang dikembalikan Future tersebut, karena ketidakcocokan tertangkap penganalisis Dart sebelum aplikasi dijalankan. Builder juga tidak perlu ditandai async, karena fungsinya tidak menunggu apa pun, melainkan hanya membaca keadaan yang dilaporkan snapshot.

List kosong adalah keadaan tersendiri#

snapshot.hasData bernilai true setiap kali snapshot.data bukan null, dan list kosong bukan null. Respons API yang berhasil tanpa satu pun hasil mengembalikan List yang ada tetapi kosong, sehingga cabang data dijalankan dan ListView.builder dengan itemCount: 0 dirender tanpa satu pun baris. Layar yang tampak benar-benar kosong seperti ini sulit dibedakan dari layar yang gagal memuat, padahal penanganannya berbeda.

Karena itu, pemeriksaan daftar.isEmpty ditulis sebagai cabang tersendiri sebelum daftar dirender, sebagaimana kerangka di atas. Pesannya menyatakan keadaan apa adanya, misalnya Belum ada catatan, dan bukan pesan error. Perbedaan tiga keadaan ini, yaitu sedang memuat, gagal, dan berhasil tanpa data, adalah syarat minimum layar yang mengambil data.

Keadaan kosong lazim muncul pada pengguna yang belum pernah membuat satu catatan pun, maupun pada layar hasil pencarian yang belum diberi kata kunci. Pesan yang tampil sebaiknya mengarahkan langkah berikutnya, misalnya menunjukkan letak tombol tambah untuk keadaan yang pertama. Keadaan ini juga terhubung dengan penguraian JSON, karena array JSON yang kosong memang diurai menjadi list kosong, bukan null.

Future dibuat sekali, bukan di dalam build#

Cara penyimpanan yang benar menggunakan late final, yaitu field yang dinyatakan tanpa nilai awal dan wajib diisi tepat satu kali. initState adalah tempat pengisiannya karena dipanggil sekali sebelum build pertama. Contoh berikut menyusun layar lengkap yang menggabungkan seluruh percabangan dengan penyimpanan yang benar.

dart Contoh: Future disimpan pada field dan diisi di initState
// tidak-dijalankan
class LayarDaftarCatatan extends StatefulWidget {
  const LayarDaftarCatatan({super.key});

  @override
  State<LayarDaftarCatatan> createState() => _LayarDaftarCatatanState();
}

class _LayarDaftarCatatanState extends State<LayarDaftarCatatan> {
  late final Future<List<Catatan>> _futureCatatan;

  @override
  void initState() {
    super.initState();
    _futureCatatan = muatCatatan();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Catatan>>(
      future: _futureCatatan,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return Center(child: Text('Gagal memuat catatan: ${snapshot.error}'));
        }
        final daftar = snapshot.data ?? const <Catatan>[];
        return ListView.builder(
          itemCount: daftar.length,
          itemBuilder: (context, index) => ListTile(
            title: Text(daftar[index].judul),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => LayarDetailCatatan(catatan: daftar[index]),
                ),
              );
            },
          ),
        );
      },
    );
  }
}

Pada contoh, build hanya meneruskan field _futureCatatan ke FutureBuilder, sehingga rebuild yang dipicu setState atau pergantian tema tidak mengganti Future yang sedang ditunggu. Callback onTap meneruskan satu objek Catatan ke LayarDetailCatatan melalui Navigator.push, menggunakan pola penerusan data antarlayar yang sama dengan pembahasan navigasi. Dengan demikian, layar daftar yang asinkron dan layar detailnya tersambung tanpa keadaan tambahan.

Kesalahan umum#

  1. Membuat Future di dalam build. Sebagaimana diperingatkan di atas, Future baru menggantikan yang lama pada setiap rebuild dan layar kembali memuat terus. Perbaikannya, simpan Future pada field dan isilah di initState.
  2. Menampilkan data tanpa menangani error. Cabang hasError yang hilang membuat kegagalan tampil sebagai layar kosong tanpa pesan, dan pengguna tidak tahu apakah harus mencoba ulang atau menyerah. Perbaikannya, urutkan pemeriksaan waiting, hasError, data, lalu list kosong.
  3. Menganggap hasData berarti list terisi. List kosong pun hasData bernilai true, sehingga layar kosong dibiarkan tanpa pesan. Perbaikannya, periksa daftar.isEmpty sebagai cabang tersendiri.
  4. Menandai builder sebagai async lalu menunggu hasilnya. Builder bukan tempat menunggu, dan await di dalamnya hanya menunda pengembalian widget tanpa mengubah cara FutureBuilder bekerja. Perbaikannya, baca keadaan dari snapshot, bukan menunggu hasilnya di dalam builder.

Ringkasan#

  • FutureBuilder menerima future dan builder, lalu memanggil builder ulang setiap keadaan Future berpindah.
  • snapshot.connectionState membedakan none, waiting, active, dan done, dan untuk satu Future keadaannya berpindah dari waiting ke done.
  • Percabangan yang lengkap menangani memuat, error, data, dan list kosong sebagai empat keadaan terpisah.
  • Future disimpan pada field yang diisi initState agar tidak dibuat ulang pada setiap rebuild.

Referensi#