Pemrograman Dart
Pemrograman Asinkron: Future, async/await, dan Stream
Pahami konsep pemrograman asinkron di Dart agar aplikasi tidak membeku saat menunggu operasi jaringan atau database menggunakan Future, async/await, Future.wait, dan Stream.

Sejauh ini, seluruh kode Dart yang kita tulis berjalan secara sinkron (synchronous): baris kedua baru dieksekusi setelah baris pertama selesai sepenuhnya. Pola ini bekerja sangat cepat untuk operasi di memori (seperti menghitung diskon atau memfilter List). Namun, bagaimana jika program kita harus mengambil data produk dari server di internet, membaca file berukuran besar di disk, atau menunggu kueri database yang memakan waktu 2 detik? Jika program berhenti total (blocking) menunggu respons jaringan selama 2 detik, antarmuka aplikasi akan membeku (freeze) dan tidak merespons sentuhan pengguna. Untuk menyelesaikan masalah ini, Dart menyediakan model Pemrograman Asinkron (Asynchronous Programming) melalui Future, async/await, dan Stream.
Yang akan dipelajari
- Memahami perbedaan eksekusi sinkron vs asinkron serta cara kerja Event Loop di Dart secara intuitif.
- Menggunakan
Future<T>untuk merepresentasikan satu nilai yang baru akan tersedia di masa mendatang. - Menulis kode asinkron yang bersih menggunakan
async,await, dan penanganan errortry-catch. - Menjalankan beberapa
Futuresecara paralel denganFuture.wait. - Mengolah aliran data berkelanjutan (multiple asynchronous events) menggunakan
Stream<T>,async*,yield, danawait for.
Konsep Sinkron vs Asinkron dan Event Loop
Dart berjalan di dalam satu alur eksekusi utama (single-threaded isolate) yang digerakkan oleh mekanisme bernama Event Loop. Bayangkan seorang barista di kedai kopi:
- Cara Sinkron (Memblokir): Barista menerima pesanan pelanggan A, menyalakan mesin kopi otomatis (butuh 60 detik), lalu berdiri diam menatap mesin kopi selama 60 detik penuh tanpa melayani pelanggan B yang hanya ingin membeli air mineral di kasir.
- Cara Asinkron (Tidak Memblokir): Barista menyalakan mesin kopi untuk pelanggan A, lalu sembari mesin bekerja di latar belakang, barista langsung melayani pelanggan B dan C. Begitu mesin kopi berbunyi (selesai), barista mengambil cangkir kopi tersebut dan menyerahkannya ke pelanggan A.
Di Dart, hasil pekerjaan yang "sedang diproses dan baru selesai nanti" dibungkus ke dalam dua tipe utama:
| Tipe Kembalian | Jumlah Nilai yang Dihasilkan | Padanan Sinkron | Contoh Kasus Nyata |
|---|---|---|---|
T (misal String) |
1 nilai langsung saat itu juga | Variabel biasa | Menggabungkan nama depan & belakang |
Future<T> |
1 nilai (atau 1 error) di masa depan | T |
Mengambil profil user lewat HTTP API, membaca file |
Stream<T> |
0, 1, atau banyak nilai secara beruntun dari waktu ke waktu | Iterable<T> / List<T> |
Lokasi GPS real-time, chat masuk, hitung mundur timer |
Bekerja dengan Future<T>, async, dan await
Sebuah objek Future<T> selalu berada dalam salah satu dari dua kondisi:
- Uncompleted (Belum selesai): operasi masih berlangsung (misal masih menunggu balasan server).
- Completed (Selesai): operasi telah tuntas, baik menghasilkan nilai bertipe
Tmaupun gagal menghasilkan objekException/Error.
Agar kita bisa menunggu hasil Future<T> dengan gaya penulisan yang urut dan mudah dibaca seperti kode sinkron, Dart menyediakan pasangan kata kunci async dan await:
- Tandai fungsi dengan kata kunci
asynctepat sebelum badan fungsi ({atau=>). Fungsi yang memiliki penandaasyncwajib memiliki tipe kembalianFuture<T>(atauFuture<void>jika tidak mengembalikan nilai). - Gunakan kata kunci
awaitdi depan pemanggilan fungsi yang mengembalikanFuture. Kata kunciawaitakan menunda kelanjutan baris di bawahnya di dalam fungsi tersebut hingga nilaiFuturesiap, tanpa membekukan bagian program yang lain!
class LayananCuaca {
Future<String> ambilPrakiraanCuaca(String kota) async {
print('[1] Menghubungi satelit cuaca untuk kota $kota...');
// Simulasi jeda jaringan selama 1 detik
await Future.delayed(const Duration(seconds: 1));
if (kota.trim().isEmpty) {
throw ArgumentError('Nama kota tidak boleh kosong.');
}
return 'Cerah Berawan, 29°C';
}
}
// Perhatikan main() ditandai `async` dan mengembalikan `Future<void>`
Future<void> main() async {
final layanan = LayananCuaca();
print('--- Aplikasi Cuaca Dimulai ---');
try {
// Tanpa await, variabel `hasil` hanya akan berisi "Instance of 'Future<String>'"
final hasil = await layanan.ambilPrakiraanCuaca('Yogyakarta');
print('[2] Data diterima -> Yogyakarta: $hasil');
// Menguji skenario gagal
await layanan.ambilPrakiraanCuaca('');
} on ArgumentError catch (e) {
print('[Error Ditangkap] ${e.message}');
} finally {
print('--- Sesi pemeriksaan cuaca selesai ---');
}
}
Output:
--- Aplikasi Cuaca Dimulai ---
[1] Menghubungi satelit cuaca untuk kota Yogyakarta...
[2] Data diterima -> Yogyakarta: Cerah Berawan, 29°C
[1] Menghubungi satelit cuaca untuk kota ...
[Error Ditangkap] Nama kota tidak boleh kosong.
--- Sesi pemeriksaan cuaca selesai ---
Menjalankan Beberapa Future Sekaligus dengan Future.wait
Jika kita memiliki 3 permintaan data independen yang masing-masing memakan waktu 1 detik, memanggil await satu per satu secara berurutan akan memakan total waktu 3 detik (1 + 1 + 1).
Apabila ketiga tugas tersebut tidak saling bergantung satu sama lain, kita dapat menjalankannya secara paralel menggunakan Future.wait([...]). Dengan cara ini, ketiganya dimulai bersamaan dan selesai hanya dalam waktu sekitar 1 detik!
Future<String> muatProfil() async {
await Future.delayed(const Duration(milliseconds: 500));
return 'Profil: Raka (ID-99)';
}
Future<int> muatSaldoDompet() async {
await Future.delayed(const Duration(milliseconds: 500));
return 350000;
}
Future<List<String>> muatNotifikasi() async {
await Future.delayed(const Duration(milliseconds: 500));
return ['Promo Akhir Pekan', 'Pesanan Dikirim'];
}
Future<void> main() async {
final stopwatch = Stopwatch()..start();
print('Memuat seluruh data dashboard secara paralel...');
// Menjalankan ketiga Future secara bersamaan
final hasilGabungan = await Future.wait([
muatProfil(),
muatSaldoDompet(),
muatNotifikasi(),
]);
stopwatch.stop();
print('Selesai dalam ${stopwatch.elapsedMilliseconds} ms:');
print('- ${hasilGabungan[0]}');
print('- Saldo: Rp${hasilGabungan[1]}');
print('- Notifikasi: ${hasilGabungan[2]}');
}
Output:
Memuat seluruh data dashboard secara paralel...
Selesai dalam 506 ms:
- Profil: Raka (ID-99)
- Saldo: Rp350000
- Notifikasi: [Promo Akhir Pekan, Pesanan Dikirim]
Aliran Data Berkelanjutan dengan Stream<T> dan await for
Jika Future<T> ibarat memesan satu paket barang (datang sekali lalu selesai), maka Stream<T> ibarat berlangganan koran atau menonton siaran langsung: data dikirimkan secara bertahap dari waktu ke waktu hingga alirannya ditutup.
Untuk membuat fungsi penghasil Stream (asynchronous generator), kita menggunakan penanda async* (dengan tanda bintang) dan mengirimkan setiap nilai menggunakan kata kunci yield. Untuk mendengarkan setiap nilai yang keluar dari Stream secara berurutan, kita menggunakan perulangan asinkron await for:
// Fungsi generator asinkron menggunakan `async*` dan `yield`
Stream<String> pantauStatusKurir(String nomorResi) async* {
final tahapan = [
'Paket dijemput kurir dari gudang penjual',
'Paket tiba di pusat sortir Jakarta',
'Paket sedang dibawa kurir menuju alamat Anda',
'Paket telah diterima oleh pemilik rumah',
];
for (final tahap in tahapan) {
await Future.delayed(const Duration(milliseconds: 300));
// Mengirimkan satu nilai ke aliran Stream tanpa menghentikan fungsi
yield '[$nomorResi] $tahap';
}
}
Future<void> main() async {
print('=== Melacak Pengiriman Real-Time ===');
// `await for` akan terus menunggu setiap data baru yang di-yield hingga Stream selesai
await for (final pembaruan in pantauStatusKurir('NUSA-7788')) {
print('Update Masuk -> $pembaruan');
}
print('=== Pelacakan Selesai (Paket Tiba) ===');
}
Output:
=== Melacak Pengiriman Real-Time ===
Update Masuk -> [NUSA-7788] Paket dijemput kurir dari gudang penjual
Update Masuk -> [NUSA-7788] Paket tiba di pusat sortir Jakarta
Update Masuk -> [NUSA-7788] Paket sedang dibawa kurir menuju alamat Anda
Update Masuk -> [NUSA-7788] Paket telah diterima oleh pemilik rumah
=== Pelacakan Selesai (Paket Tiba) ===
Coba sendiri
Buka proyek VS Code Anda dan buat file bin/uji_async.dart:
- Salin kode
LayananCuacadi bagian awal materi ini ke dalambin/uji_async.dart. - Di dalam
main(), hapus kata kunciawaitdi depanlayanan.ambilPrakiraanCuaca('Yogyakarta')sehingga menjadi:final hasil = layanan.ambilPrakiraanCuaca('Yogyakarta'); print('[2] Data diterima -> Yogyakarta: $hasil'); - Jalankan
dart run bin/uji_async.dartdan amati keluarannya: baris[2]langsung mencetakInstance of 'Future<String>'bahkan sebelum jeda 1 detik selesai! - Kembalikan kata kunci
awaittersebut agar program kembali menunggu data cuaca yang sebenarnya.
Kesalahan yang sering terjadi
| Kesalahan Pemula | Mengapa Bermasalah? | Solusi yang Benar |
|---|---|---|
Lupa menulis await saat memanggil fungsi yang mengembalikan Future<T> |
Variabel hanya akan menampung objek Future<T> yang belum selesai, dan potensi exception di dalamnya bisa lolos dari blok try-catch |
Selalu gunakan await di depan pemanggilan fungsi asinkron |
Menggunakan await di dalam fungsi biasa yang belum diberi label async |
Compiler akan menolak dengan pesan The await expression can only be used in an async function | Tambahkan kata kunci async sebelum tanda { pada deklarasi fungsi |
Menulis tipe kembalian void (bukan Future<void>) pada fungsi async |
Pemanggil fungsi tidak bisa melakukan await pada fungsi tersebut karena tipe kembaliannya bukan Future |
Selalu tulis Future<void> untuk fungsi async yang tidak mengembalikan nilai |
Menggunakan perulangan for-in biasa untuk membaca Stream<T> |
for-in hanya berlaku untuk koleksi sinkron (Iterable seperti List/Set), bukan Stream |
Gunakan await for (final item in aliranStream) di dalam fungsi async |
Kaitannya dengan Flutter / Dunia Nyata: Pemrograman asinkron adalah tulang punggung aplikasi Flutter! Ketika aplikasi Anda mengambil daftar berita dari REST API (menggunakan package
httpataudio), membaca database lokal (sqflite/drift), atau menyimpan sesi login (shared_preferences), semuanya mengembalikanFuture<T>. Di Flutter, kita juga memiliki widget bawaan bernamaFutureBuilderdanStreamBuilder(yang sangat populer saat terhubung ke Firebase Firestore) yang secara otomatis menggambar ulang layar setiap kaliFutureselesai atau setiap kaliStreammemancarkan data baru.
Latihan
- Simulasi Verifikasi OTP (
Future&try-catch): Buat fungsiFuture<String> verifikasiOtp(String kodeInput)yang melakukan jeda 400 milidetik (Future.delayed). JikakodeInput == '2468', kembalikan'Autentikasi Berhasil'. Jika tidak, lemparFormatException('Kode OTP salah atau kadaluarsa'). Panggil fungsi tersebut dimain()denganasync/awaitdan tangani kesalahannya dengantry-catch. - Pencarian Harga Termurah Paralel (
Future.wait): Buat tiga fungsi asinkron yang mensimulasikan pengecekan harga buku yang sama di tiga toko berbeda:cekHargaTokoA()(mengembalikan85000),cekHargaTokoB()(79000), dancekHargaTokoC()(82000), masing-masing dengan jeda 300 ms. GunakanFuture.waitdimain()untuk mengambil ketiga harga secara paralel, lalu cari dan cetak harga yang paling murah. - Stream Hitung Mundur Peluncuran (
async*&yield): Buat fungsiStream<int> hitungMundur(int detikAwal) async*yang setiap 200 milidetik memancarkan angka daridetikAwalturun hingga1. Di dalammain(), konsumsi stream tersebut menggunakanawait foruntuk mencetak'H-3...','H-2...','H-1...', lalu cetak'Meluncur!'setelah stream selesai.
Kunci jawaban dan pembahasan
Latihan 1
Future<String> verifikasiOtp(String kodeInput) async {
await Future.delayed(const Duration(milliseconds: 400));
if (kodeInput == '2468') {
return 'Autentikasi Berhasil! Selamat datang.';
}
throw const FormatException('Kode OTP salah atau sudah kadaluarsa.');
}
Future<void> main() async {
for (final percobaan in ['1111', '2468']) {
try {
print('Memeriksa OTP "$percobaan"...');
final pesan = await verifikasiOtp(percobaan);
print('[SUKSES] $pesan');
} on FormatException catch (e) {
print('[GAGAL] ${e.message}');
}
}
}
Pembahasan: Ketika sebuah fungsi async melempar exception dengan throw, Dart secara otomatis membungkus exception tersebut ke dalam Future yang gagal (completed with error). Saat dipanggil dengan await, kita bisa menangkapnya menggunakan try-on-catch persis seperti menangani fungsi sinkron.
Latihan 2
Future<int> cekHargaTokoA() async {
await Future.delayed(const Duration(milliseconds: 300));
return 85000;
}
Future<int> cekHargaTokoB() async {
await Future.delayed(const Duration(milliseconds: 300));
return 79000;
}
Future<int> cekHargaTokoC() async {
await Future.delayed(const Duration(milliseconds: 300));
return 82000;
}
Future<void> main() async {
final daftarHarga = await Future.wait<int>([
cekHargaTokoA(),
cekHargaTokoB(),
cekHargaTokoC(),
]);
final hargaTermurah = daftarHarga.reduce((a, b) => a < b ? a : b);
print('Daftar harga dari 3 toko : $daftarHarga');
print('Harga terbaik ditemukan : Rp$hargaTermurah');
}
Pembahasan: Future.wait<int>([...]) mengembalikan Future<List<int>> yang berisi hasil ketiga fungsi sesuai urutan pendaftarannya. Setelah di-await, kita langsung mendapatkan List<int> dan dapat mencari nilai terkecil dengan method .reduce().
Latihan 3
Stream<int> hitungMundur(int detikAwal) async* {
for (var angka = detikAwal; angka >= 1; angka--) {
await Future.delayed(const Duration(milliseconds: 200));
yield angka;
}
}
Future<void> main() async {
print('Persiapan peluncuran roket...');
await for (final detik in hitungMundur(3)) {
print('H-$detik...');
}
print('Meluncur!');
}
Pembahasan: Fungsi hitungMundur menggunakan async* sehingga menghasilkan Stream<int>. Setiap kali baris yield angka; dijalankan, nilai angka langsung dikirimkan ke blok await for di main(). Setelah perulangan for di dalam generator selesai, aliran Stream otomatis ditutup dan eksekusi di main() berlanjut ke baris print('Meluncur!').
Diskusi (0)
Kami menyimpan nama (opsional), komentar, dan hash teknis (bukan alamat IP asli) untuk mencegah spam. Cookie fungsional dipakai agar Anda dapat mengedit atau menghapus komentar sendiri dari browser ini. Untuk permintaan penghapusan, gunakan halaman kontak.
Belum ada komentar. Mulai diskusinya.