Future

Future sebagai nilai yang datang kemudian dan cara menunggunya.

Daftar isi

Operasi seperti membaca berkas, memanggil jaringan, atau sekadar menunggu jeda tidak memberikan nilainya seketika, sehingga kode yang menunggu tidak boleh menghentikan seluruh program. Dart memodelkan keadaan tersebut dengan Future, yaitu objek yang mewakili satu nilai yang tersedia di kemudian hari beserta kegagalan yang mungkin menyertainya. Topik ini membahas cara membuat Future melalui Future.value, Future.delayed, dan Completer, cara menerima nilainya dengan then, cara menangkap kegagalannya dengan catchError, serta cara menunggu banyak Future sekaligus dengan Future.wait. Semua contoh dapat dijalankan di terminal tanpa package tambahan karena Future berada di dart:async yang sudah bawaan.

Future sebagai nilai yang datang kemudian#

Sebuah Future<T> adalah tempat penyimpanan untuk satu nilai bertipe T yang belum tersedia pada saat objeknya dibuat. Fungsi yang pekerjaannya tertunda mengembalikan Future<T>, sedangkan pemanggil mendaftarkan penerima hasil melalui then. Pendaftaran itu tidak menunggu, sehingga baris kode setelah pemanggilan tetap dijalankan lebih dahulu, dan callback pada then baru dieksekusi ketika nilainya tiba. Urutan inilah yang membedakan kode asinkron dari kode sinkron biasa.

dart pertama.dart
Future<int> nilaiKemudian() {
  return Future.value(10);
}

void main() {
  print("program dimulai");
  nilaiKemudian().then((nilai) {
    print("nilai tiba: $nilai");
  });
  print("baris ini dicetak lebih dahulu");
}
bash
# menjalankan program dari folder topik
dart run pertama.dart
text Keluaran
program dimulai
baris ini dicetak lebih dahulu
nilai tiba: 10

Fungsi nilaiKemudian menggunakan Future.value(10), yaitu Future yang sudah lengkap sejak awal dengan nilai 10. Meskipun begitu, callback pada then tetap tidak dijalankan di tengah pemanggilan, melainkan setelah seluruh kode sinkron pada fungsi main selesai. Keluaran menunjukkan urutan tersebut dengan jelas, sehingga kesimpulannya dapat dirumuskan: then mendaftarkan reaksi atas hasil, bukan permintaan untuk menunggu di tempat. Perilaku ini berlaku sama untuk Future yang sudah lengkap maupun Future yang baru akan terisi.

Membuat Future dengan jeda#

Future.delayed membuat Future yang nilai barunya muncul setelah durasi tertentu, sehingga contoh latihan dapat menyimulasikan operasi lambat tanpa jaringan atau berkas sungguhan. Argumen pertamanya berupa Duration, sedangkan argumen kedua berupa fungsi yang menghasilkan nilai ketika jeda selesai. Bentuk lain yang sering digunakan adalah Future(computation), yaitu Future yang menjalankan fungsi komputasinya di antrean event, sehingga pemanggil tetap mendapat alur kendali lebih dahulu. Nilai const pada Duration menyusun durasi pada saat kompilasi, sehingga penulisan const Duration(...) menjadi kebiasaan yang baik untuk jeda yang tetap. Satuan lain seperti seconds dan minutes juga tersedia apabila contoh latihan membutuhkannya, dan pola kodenya tidak berubah antarsatuan.

dart tertunda.dart
void main() {
  print("permintaan dikirim");
  Future.delayed(const Duration(milliseconds: 50), () {
    return 24;
  }).then((nilai) {
    print("nilai tiba: $nilai");
  });
  print("program melanjutkan pekerjaan lain");
}
bash
# menjalankan program dari folder topik
dart run tertunda.dart
text Keluaran
permintaan dikirim
program melanjutkan pekerjaan lain
nilai tiba: 24

Program mengirim permintaan, melanjutkan pekerjaan sinkronnya, dan baru mencetak nilai ketika jeda 50 milidetik habis. Selama jeda tersebut tidak ada satu baris kode pun yang tertahan, karena then hanya mendaftarkan penerima hasil. Pola ini adalah model dasar semua operasi asinkron di Dart, mulai dari pembacaan berkas pada dart:io hingga permintaan HTTP, yang semuanya mengembalikan Future dengan mekanisme yang sama.

Menangkap kegagalan dengan catchError#

Sebuah Future dapat berakhir dengan error, bukan dengan nilai, apabila pekerjaan di baliknya gagal. Error semacam itu tidak dimunculkan sebagai exception biasa di baris pemanggilan, melainkan dibawa oleh Future itu sendiri dan diteruskan ke catchError yang terdaftar. Apabila tidak ada catchError maupun penanganan lain, error tersebut berakhir sebagai unhandled exception yang melaporkan dirinya pada saat program berjalan. Penempatan catchError setelah rantai then membuat satu penanganan melayani seluruh langkah di depannya. Callback pada catchError boleh pula mengembalikan nilai pengganti, sehingga Future hasil rantai berakhir normal dengan nilai tersebut apabila penanganannya menemukan cara keluar.

dart gagal.dart
Future<int> uraikan(String teks) {
  return Future(() => int.parse(teks));
}

void main() {
  uraikan("42").then((nilai) {
    print("berhasil: $nilai");
  }).catchError((e) {
    print("gagal diuraikan: ${e.runtimeType}");
  });
  uraikan("dua").then((nilai) {
    print("berhasil: $nilai");
  }).catchError((e) {
    print("gagal diuraikan: ${e.runtimeType}");
  });
  print("pemrosesan masukan dimulai");
}
bash
# menjalankan program dari folder topik
dart run gagal.dart
text Keluaran
pemrosesan masukan dimulai
berhasil: 42
gagal diuraikan: FormatException

Kedua pemanggilan uraikan dilakukan berurutan, dan keduanya selesai secara asinkron sebelum program berakhir. Masukan "42" menghasilkan jalur sukses pada then, sedangkan masukan "dua" membuat int.parse melempar FormatException yang dibawa Future dan jatuh ke catchError. Perhatikan bahwa pesan pemrosesan masukan dimulai dicetak paling awal, karena seluruh pendaftaran callback berlangsung seketika tanpa menunggu hasil apa pun. Dengan pendaftaran seketika tersebut, tidak ada celah waktu ketika error lewat tanpa penerima.

Completer untuk Future yang diisi manual#

Completer menyediakan cara membuat Future yang penyelesaiannya berada di tangan penulis kode, bukan di fungsi pabrikan seperti Future.delayed. Objek Completer<T> membagi satu pekerjaan menjadi dua sisi, yaitu sisi konsumen yang memegang completer.future dan sisi produsen yang memanggil complete untuk mengisinya. Pola ini berguna ketika nilai berasal dari sumber yang tidak berbentuk Future sejak awal, misalnya pesan dari pendengar atau hasil callback pustaka lama. Dengan Completer, kejadian yang berbasis callback dapat dibungkus menjadi Future yang gaya penggunaannya seragam. Setiap Completer hanya boleh diselesaikan satu kali, karena pemanggilan complete yang berulang memunculkan StateError.

dart completer.dart
import 'dart:async';

void tampilkanHasil(Future<double> hasil) {
  hasil.then((suhu) {
    print("suhu tercatat: $suhu");
  });
}

void main() {
  final completer = Completer<double>();
  tampilkanHasil(completer.future);
  print("sensor diaktifkan, menunggu data");
  Future.delayed(const Duration(milliseconds: 50), () {
    completer.complete(21.5);
  });
}
bash
# menjalankan program dari folder topik
dart run completer.dart
text Keluaran
sensor diaktifkan, menunggu data
suhu tercatat: 21.5

Fungsi tampilkanHasil hanya menerima sebuah Future<double> dan tidak mengetahui bagaimana nilainya diisi, sedangkan bagian main mengisinya 50 milidetik kemudian melalui completer.complete(21.5). Kedua sisi tersebut terhubung tanpa saling mengenal detail mekanismenya, karena satu-satunya kontrak di antara keduanya adalah Future yang dibagikan. Apabila kegagalan perlu disampaikan, completer.completeError mengisi Future dengan error yang berakhir di catchError milik konsumen, sebagaimana error dari pekerjaan asinkron biasa.

Ketika pekerjaan yang tertunda lebih dari satu, Future.wait menerima daftar Future dan menghasilkan satu Future baru yang berisi list hasil dalam urutan daftarnya. Future gabungan itu lengkap setelah seluruh anggotanya selesai, sehingga hasil yang datang lebih cepat tetap menunggu yang lebih lambat sebelum keduanya diserahkan bersama. Seluruh anggota daftarnya sudah berjalan sejak daftar itu dibentuk, sehingga Future.wait benar-benar menunggu dan tidak memulai pekerjaan apa pun. Apabila salah satu anggota berakhir dengan error, Future gabungan ikut berakhir dengan error tersebut, dan penanganannya tetap berada di catchError. Pola ini menjadi dasar menjalankan banyak permintaan secara bersamaan sebelum dipadukan menjadi satu hasil.

dart menunggu-semua.dart
Future<int> pengukuran(int kelipatan, int nilai) {
  return Future.delayed(Duration(milliseconds: kelipatan * 50), () => nilai);
}

void main() {
  final kumpulan = Future.wait([
    pengukuran(3, 10),
    pengukuran(2, 24),
    pengukuran(1, 8),
  ]);
  kumpulan.then((hasil) {
    print("semua pengukuran selesai");
    print("hasil: $hasil");
  });
}
bash
# menjalankan program dari folder topik
dart run menunggu-semua.dart
text Keluaran
semua pengukuran selesai
hasil: [10, 24, 8]

Tiga pengukuran sengaja diberi jeda berbeda, yaitu 150, 100, dan 50 milidetik, sehingga yang paling lambat justru yang muncul pertama di daftar. Keluaran tetap menampilkan [10, 24, 8] karena Future.wait menyusun hasil menurut urutan daftar masukannya, bukan menurut urutan penyelesaian. Jaminan urutan seperti ini membuat hasil gabungan langsung dapat dipasangkan dengan daftar permintaannya tanpa pencocokan ulang.

Ringkasan#

  • Future<T> mewakili satu nilai bertipe T yang tersedia di kemudian hari, dan then mendaftarkan penerima nilainya tanpa menahan alur kode.
  • Future.value, Future.delayed, dan Future(computation) membuat Future dari nilai jadi, jeda waktu, dan komputasi yang ditunda.
  • Error pada Future dibawa sampai catchError, dan error tanpa penanganan berakhir sebagai unhandled exception.
  • Completer memisahkan sisi konsumen yang memegang completer.future dari sisi produsen yang memanggil complete.
  • Future.wait menunggu banyak Future sekaligus dan menyusun hasil menurut urutan daftarnya, bukan urutan penyelesaiannya.

Referensi#