asyncuiloadingMenengah4 mnt baca

Pola UI Async: Refresh & Loading

RefreshIndicator, shimmer skeleton, dan retry button.

Masalahnya: Data Butuh Waktu

Request API tidak instan. Di antara "pengguna membuka halaman" dan "data muncul" ada jeda sepersekian detik sampai beberapa detik. Kalau jeda ini tidak ditangani, pengguna melihat layar kosong dan mengira aplikasi rusak atau hang. Pola UI async adalah jawaban standar industri untuk tiga momen: saat menunggu (loading), saat gagal (error), dan saat pengguna ingin data terbaru (refresh).

Aplikasi yang bagus tidak cuma "bisa" memuat data, tapi membuat masa tunggunya terasa wajar dan kegagalannya bisa dipulihkan.

Fondasi: FutureBuilder yang Menangani Semua State

Sebelum pola lanjutan, pastikan FutureBuilder-mu menangani empat state, bukan cuma dua:

dart
FutureBuilder<List<Produk>>(
  future: _future,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    if (snapshot.hasError) {
      return _ErrorView(
        pesan: 'Gagal memuat data',
        onRetry: () => setState(() => _future = ambilProduk()),
      );
    }
    final data = snapshot.data ?? [];
    if (data.isEmpty) {
      return const Center(child: Text('Belum ada produk'));
    }
    return ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, i) => ListTile(title: Text(data[i].nama)),
    );
  },
)

Perhatikan state keempat yang paling sering dilupakan: data kosong. List kosong bukan error, jadi tampilkan pesan khusus, bukan layar blank yang membingungkan.

Contoh 1: Pull-to-Refresh dengan RefreshIndicator

Gesture tarik-ke-bawah untuk memuat ulang adalah ekspektasi standar pengguna mobile:

dart
RefreshIndicator(
  onRefresh: () async {
    final baru = await ambilProduk();
    setState(() => _produk = baru);
  },
  child: ListView.builder(
    itemCount: _produk.length,
    itemBuilder: (context, i) => ListTile(title: Text(_produk[i].nama)),
  ),
)

RefreshIndicator hanya bekerja kalau child-nya widget yang bisa di-scroll. Kalau list-mu pendek dan tidak bisa di-scroll, beri physics: const AlwaysScrollableScrollPhysics() pada ListView supaya gesture tarik tetap terdeteksi walau kontennya sedikit.

Contoh 2: Skeleton Loading (Shimmer)

Spinner di tengah layar memberi tahu "tunggu", tapi tidak memberi tahu "hasilnya akan seperti apa". Skeleton menampilkan bayangan bentuk konten sehingga transisinya terasa mulus:

dart
// flutter pub add shimmer
Shimmer.fromColors(
  baseColor: Colors.grey[300]!,
  highlightColor: Colors.grey[100]!,
  child: Column(
    children: List.generate(3, (_) => Container(
      height: 80,
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
      ),
    )),
  ),
)

Bentuk skeleton sebaiknya mirip item aslinya (tinggi baris, posisi gambar, panjang teks). Skeleton yang asal-asalan malah membingungkan karena layout "melompat" saat data asli muncul.

Contoh 3: Error View dengan Tombol Retry

dart
class _ErrorView extends StatelessWidget {
  final String pesan;
  final VoidCallback onRetry;
  const _ErrorView({required this.pesan, required this.onRetry});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.cloud_off, size: 48, color: Colors.grey),
          const SizedBox(height: 12),
          Text(pesan),
          const SizedBox(height: 12),
          ElevatedButton(onPressed: onRetry, child: const Text('Coba Lagi')),
        ],
      ),
    );
  }
}

Kunci retry yang benar: buat ulang future-nya (_future = ambilProduk()), bukan cuma setState(() {}). Future yang sudah selesai dengan error tidak akan jalan lagi sendiri.

Edge Case

  1. Jangan tampilkan shimmer untuk loading kilat. Kalau request selesai dalam 200ms, skeleton yang berkedip sepersekian detik terlihat seperti bug. Pertimbangkan menampilkan skeleton hanya jika loading lebih dari 300ms.
  2. RefreshIndicator dan FutureBuilder harus sinkron. Setelah refresh manual, update juga future yang dipakai FutureBuilder supaya tidak ada dua sumber state yang bertengkar.
  3. Mode gelap. Warna shimmer grey[300] di dark mode terlihat aneh dan terlalu terang. Sesuaikan baseColor dengan Theme.of(context).brightness.

Catatan teknis: Standar aplikasi produksi: FutureBuilder untuk state (loading/error/empty/data), RefreshIndicator untuk reload manual, tombol retry untuk error. Ketiganya saling melengkapi, bukan saling menggantikan.

Kapan JANGAN Pakai Pola Ini

  • FutureBuilder untuk data yang sering berubah (misal status pesanan realtime atau harga saham). Future jalan sekali lalu selesai; untuk data yang mengalir terus pakai StreamBuilder atau state management reaktif.
  • Pull-to-refresh di halaman yang tidak butuh refresh manual. Kalau datanya statis atau selalu fresh setiap navigasi, RefreshIndicator cuma dekorasi yang membingungkan pengguna.

Tantangan

Retry saat error

Modifikasi FutureBuilder produkmu: saat hasError, tampilkan tombol 'Coba Lagi' yang membuat ulang future dan memanggil setState.

Kuis Bab

Uji pemahamanmu: Async, JSON & HTTP

Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".

1.Apa itu Future dalam Dart?

2.Apa kesalahan umum memakai FutureBuilder?

3.Kapan memakai StreamBuilder dibanding FutureBuilder?

4.Apa keuntungan model class dengan fromJson dibanding akses Map langsung?

5.Header apa yang wajib saat POST body JSON?