Asynchronous Programming #

Aplikasi Flutter yang baik tidak pernah membekukan UI saat menunggu data dari server, membaca file, atau melakukan komputasi berat. Kunci dari ini semua adalah pemrograman asinkron — kemampuan Dart untuk menjalankan operasi yang membutuhkan waktu tanpa memblokir thread utama. Artikel ini membahas seluruh model asinkron Dart dari fondasi hingga pola-pola tingkat lanjut.

Event Loop — Jantung Concurrency Dart #

Dart berjalan dalam satu thread tunggal di setiap Isolate. Tidak ada shared-memory threading seperti Java atau C++. Sebaliknya, Dart menggunakan event loop untuk mengelola eksekusi kode secara non-blocking.

Dart Isolate (main):

  +------------------------------------------+
  |              Event Loop                  |
  |                                          |
  |  Microtask Queue (prioritas tinggi)      |
  |  [task1] [task2] [task3]                 |
  |                                          |
  |  Event Queue (prioritas normal)          |
  |  [I/O event] [timer] [UI event]          |
  +------------------------------------------+
              |
              v
  Event Loop mengambil task satu per satu
  dan mengeksekusinya di Dart thread

Dua Queue yang Berbeda #

Dart memiliki dua antrian yang diproses dengan prioritas berbeda:

Microtask Queue — diproses segera setelah kode sinkron saat ini selesai, sebelum event apapun dari Event Queue:

import 'dart:async';

void main() {
  print('1 - sinkron');

  Future.microtask(() => print('2 - microtask'));
  Future(() => print('3 - event queue'));

  scheduleMicrotask(() => print('4 - microtask juga'));

  print('5 - sinkron');
}

// Output:
// 1 - sinkron
// 5 - sinkron
// 2 - microtask      <-- microtask diproses sebelum event queue
// 4 - microtask juga
// 3 - event queue

Event Queue — berisi event dari I/O, timer, user input, dan operasi async lainnya. Diproses setelah semua microtask habis.

Hindari menyumbat Microtask Queue. Jika kamu terus menambahkan microtask dalam loop, Event Queue tidak akan pernah diproses — UI akan membeku. Future.microtask sebaiknya digunakan hanya untuk operasi sangat ringan yang perlu dieksekusi sesegera mungkin.

Future — Nilai yang Akan Datang #

Future<T> adalah representasi dari nilai bertipe T yang belum tersedia saat ini, tapi akan tersedia di masa depan. Future memiliki tiga state:

Future lifecycle:

  Uncompleted          Completed with value    Completed with error
  (pending)            (success)               (failure)
      |                      |                       |
  Masih menunggu       Future.value(data)      Future.error(exception)
  operasi selesai      Siap digunakan          Perlu ditangani

Membuat Future #

// Future yang selesai setelah delay
Future<String> fetchData() {
  return Future.delayed(
    const Duration(seconds: 2),
    () => 'Data berhasil dimuat',
  );
}

// Future yang langsung selesai dengan nilai
Future<int> getCount() => Future.value(42);

// Future yang langsung selesai dengan error
Future<String> alwaysFails() =>
    Future.error(Exception('Terjadi kesalahan'));

// Future dari operasi async (paling umum)
Future<List<User>> loadUsers() async {
  final response = await http.get(Uri.parse('https://api.example.com/users'));
  return parseUsers(response.body);
}

Future API — then, catchError, whenComplete #

Sebelum async/await tersedia, Future digunakan dengan callback API:

fetchData()
    .then((data) {
      print('Berhasil: $data');
      return processData(data);   // bisa return Future lain untuk chaining
    })
    .then((result) {
      print('Hasil: $result');
    })
    .catchError((error) {
      print('Error: $error');
    })
    .whenComplete(() {
      print('Selalu dijalankan, apapun hasilnya');
    });

Meski valid, pola callback ini bisa menjadi sulit dibaca ketika ada banyak operasi berantai — inilah mengapa async/await lebih direkomendasikan.


async/await — Kode Asinkron yang Terbaca Sinkron #

async dan await adalah syntactic sugar di atas Future yang membuat kode asinkron terlihat dan terasa seperti kode sinkron biasa.

Fungsi async #

Menambahkan async ke fungsi membuatnya otomatis mengembalikan Future:

// Fungsi sinkron biasa
String greet() => 'Halo!';

// Fungsi async -- otomatis return Future<String>
Future<String> greetAsync() async => 'Halo!';

// Keduanya ekuivalen dalam hasil akhir,
// tapi greetAsync() bisa menggunakan await di dalamnya

Keyword await #

await menghentikan eksekusi fungsi async saat ini dan menunggu Future selesai, tanpa memblokir thread:

Future<void> loadUserProfile(String userId) async {
  // Tanpa await, semua ini dieksekusi bersamaan (tidak berurutan)
  // Dengan await, eksekusi berhenti di setiap baris hingga Future selesai

  print('Memuat profil...');

  final user = await fetchUser(userId);            // tunggu user
  final posts = await fetchPosts(user.id);         // tunggu posts
  final followers = await fetchFollowers(user.id); // tunggu followers

  print('Profil ${user.name}: ${posts.length} post, '
      '${followers.length} follower');
}

Error Handling dengan try/catch #

Future<void> submitForm(FormData data) async {
  try {
    final token = await authService.getToken();
    final response = await apiClient.post('/submit', data, token: token);

    if (response.statusCode == 200) {
      await localDb.saveRecord(response.body);
      showSuccessNotification();
    } else {
      throw ApiException('Server error: ${response.statusCode}');
    }
  } on NetworkException catch (e) {
    // Tangani error jaringan secara spesifik
    showNetworkError(e.message);
  } on ApiException catch (e) {
    // Tangani API error
    showApiError(e.message);
  } catch (e, stackTrace) {
    // Tangani semua error lain
    logger.error('Unexpected error', e, stackTrace);
    showGenericError();
  } finally {
    // Selalu dijalankan — cocok untuk cleanup
    hideLoadingIndicator();
  }
}

Menjalankan Future Secara Paralel #

Salah satu kesalahan umum adalah menjalankan operasi yang bisa paralel secara berurutan:

// TIDAK EFISIEN — sequential, total waktu = 2s + 3s = 5s
Future<void> loadDashboardSlow() async {
  final user = await fetchUser();        // 2 detik
  final stats = await fetchStats();      // 3 detik
  updateUI(user, stats);
}

// EFISIEN — parallel, total waktu = max(2s, 3s) = 3s
Future<void> loadDashboardFast() async {
  final results = await Future.wait([
    fetchUser(),    // berjalan paralel
    fetchStats(),   // berjalan paralel
  ]);

  final user = results[0] as User;
  final stats = results[1] as Stats;
  updateUI(user, stats);
}

// Dart 3: Future.wait pada Record (lebih type-safe)
Future<void> loadDashboardTypeSafe() async {
  final (user, stats) = await (fetchUser(), fetchStats()).wait;
  updateUI(user, stats);
}

Future.any dan Future.timeout #

// Future.any — selesai ketika SALAH SATU Future selesai
final fastest = await Future.any([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3(),
]);

// timeout — throw TimeoutException jika terlalu lama
final data = await fetchData().timeout(
  const Duration(seconds: 10),
  onTimeout: () => throw TimeoutException('Koneksi timeout'),
);

Stream — Urutan Nilai Asinkron #

Jika Future<T> adalah satu nilai yang akan datang, maka Stream<T> adalah urutan nilai yang datang dari waktu ke waktu. Stream sangat cocok untuk real-time data, event, dan operasi yang menghasilkan banyak nilai.

Future<T>:   ---[nilai]----> selesai
Stream<T>:   ---[v1]--[v2]--[v3]--[v4]---> selesai (atau terus)

Membuat Stream #

// Stream sederhana dengan async*
Stream<int> countdown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;   // emit satu nilai ke stream
  }
}

// Stream dari List (sudah ada)
Stream<String> fromList = Stream.fromIterable(['a', 'b', 'c']);

// Stream periodik
Stream<int> ticker = Stream.periodic(
  const Duration(seconds: 1),
  (count) => count,
);

// StreamController untuk full control
final controller = StreamController<String>();
controller.sink.add('pesan pertama');
controller.sink.add('pesan kedua');
controller.close();

Jenis Stream #

// Single-subscription stream (default):
// Hanya bisa di-listen SATU kali
// Cocok untuk: file reading, HTTP response, isolate ports

final singleStream = Stream.fromIterable([1, 2, 3]);
singleStream.listen((v) => print(v));
// singleStream.listen((v) => print(v)); // ERROR: sudah di-listen!

// Broadcast stream:
// Bisa di-listen oleh banyak listener secara bersamaan
// Cocok untuk: event bus, user actions, real-time updates

final controller = StreamController<String>.broadcast();
controller.stream.listen((v) => print('Listener 1: $v'));
controller.stream.listen((v) => print('Listener 2: $v'));
controller.sink.add('event');
// Output:
// Listener 1: event
// Listener 2: event

Mengkonsumsi Stream #

// await for — paling idiomatic
Future<void> processStream() async {
  final stream = countdown(5);

  await for (final nilai in stream) {
    print('Countdown: $nilai');
  }
  print('Selesai!');
}

// listen() — lebih fleksibel untuk Flutter widgets
final subscription = stream.listen(
  (data) => print('Data: $data'),
  onError: (error) => print('Error: $error'),
  onDone: () => print('Stream selesai'),
  cancelOnError: false,  // lanjutkan meski ada error
);

// Jangan lupa cancel subscription!
subscription.cancel();

Stream Transformations #

final angka = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

angka
    .where((n) => n % 2 == 0)           // filter genap: 2,4,6,8,10
    .map((n) => n * n)                  // kuadratkan: 4,16,36,64,100
    .take(3)                            // ambil 3 pertama: 4,16,36
    .listen(print);

// debounce -- tunggu X ms setelah event terakhir (berguna untuk search)
searchInput
    .debounce((_) => TimerStream(true, const Duration(milliseconds: 300)))
    .distinct()                         // skip jika nilai sama dengan sebelumnya
    .asyncMap((query) => searchApi(query))  // mapping ke Future
    .listen(updateResults);

Stream di Flutter — StreamBuilder #

StreamBuilder<int>(
  stream: countdown(10),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    if (!snapshot.hasData) {
      return const CircularProgressIndicator();
    }

    return Text(
      'Countdown: ${snapshot.data}',
      style: const TextStyle(fontSize: 48),
    );
  },
)

Isolate — Concurrency Sesungguhnya #

async/await dan Stream hanya memberikan concurrency, bukan parallelism — mereka tidak benar-benar berjalan bersamaan, hanya bergantian secara efisien. Untuk parallelism yang sesungguhnya (memanfaatkan multiple CPU core), Dart menyediakan Isolate.

async/await (concurrency):
  UI  ─────────────────────────────────────>
                  ^yield   ^resume
               (bergantian, satu thread)

Isolate (parallelism):
  Main ──────────────────────────────────>
  Worker ──────────────────────────────> (thread berbeda, CPU core berbeda)

compute() — Isolate yang Mudah #

Flutter menyediakan fungsi compute() sebagai cara termudah menjalankan fungsi di Isolate background:

// Fungsi yang MAHAL untuk dijalankan di UI thread
List<Product> parseProducts(String jsonData) {
  final list = jsonDecode(jsonData) as List;
  return list.map((e) => Product.fromJson(e)).toList();
  // Parsing 10.000 item JSON bisa memakan 200ms+
  // Ini akan menyebabkan jank jika dijalankan di UI thread!
}

// Gunakan compute() untuk pindahkan ke Isolate background
Future<void> loadProducts() async {
  final jsonData = await http.read(Uri.parse('/products'));

  // compute() menjalankan parseProducts di Isolate background
  // UI thread tetap responsive selama parsing berlangsung
  final products = await compute(parseProducts, jsonData);

  setState(() => _products = products);
}

Isolate.run() — Dart 3 API Baru #

// Isolate.run() adalah API modern yang lebih simpel dari compute()
Future<void> processImage(File imageFile) async {
  final bytes = await imageFile.readAsBytes();

  // Jalankan image processing di Isolate background
  final processed = await Isolate.run(() {
    return applyFilters(bytes);   // operasi berat
  });

  setState(() => _image = processed);
}

Isolate Manual dengan SendPort #

Untuk komunikasi dua arah yang lebih kompleks:

Future<void> startWorker() async {
  final receivePort = ReceivePort();

  // Spawn isolate baru
  await Isolate.spawn(workerIsolate, receivePort.sendPort);

  // Terima pesan dari worker
  await for (final message in receivePort) {
    if (message is String) {
      print('Worker berkata: $message');
    } else if (message == null) {
      receivePort.close();  // worker selesai
      break;
    }
  }
}

// Fungsi yang berjalan di Isolate terpisah
void workerIsolate(SendPort sendPort) {
  sendPort.send('Memulai pekerjaan berat...');

  // Simulasi komputasi berat
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }

  sendPort.send('Hasil: $sum');
  sendPort.send(null);   // sinyal selesai
}

Kapan Menggunakan Apa? #

Operasi yang membutuhkan waktu -- gunakan:

  Future + async/await:
    ✓ HTTP request
    ✓ File I/O
    ✓ Database query
    ✓ SharedPreferences
    ✓ Semua operasi async yang mengembalikan SATU nilai

  Stream:
    ✓ Real-time data (WebSocket, Firebase)
    ✓ User events (search input, form changes)
    ✓ File besar yang dibaca bertahap
    ✓ Sensor data (GPS, accelerometer)
    ✓ Operasi yang menghasilkan BANYAK nilai dari waktu ke waktu

  Isolate (compute / Isolate.run):
    ✓ JSON parsing untuk dataset besar (>1000 item)
    ✓ Image processing
    ✓ Enkripsi/dekripsi
    ✓ Komputasi matematika berat
    ✓ Semua operasi CPU-intensive yang menyebabkan jank

Ringkasan #

  • Dart menggunakan event loop dengan dua queue: Microtask Queue (prioritas tinggi) dan Event Queue (prioritas normal). Semua kode berjalan di satu thread tanpa shared memory.
  • Future merepresentasikan satu nilai yang akan datang. Gunakan async/await untuk menulis kode asinkron yang mudah dibaca seperti kode sinkron.
  • Gunakan Future.wait() atau (f1, f2).wait (Dart 3) untuk menjalankan beberapa Future secara paralel dan mempersingkat total waktu tunggu.
  • Stream merepresentasikan urutan nilai yang datang dari waktu ke waktu. Ada dua jenis: single-subscription (satu listener) dan broadcast (banyak listener).
  • Di Flutter, konsumsi Stream dengan StreamBuilder untuk memperbarui UI secara reaktif.
  • Isolate memberikan parallelism sejati memanfaatkan multiple CPU core — gunakan compute() atau Isolate.run() untuk operasi CPU-intensive yang bisa menyebabkan jank.
  • Gunakan Future/Stream untuk operasi I/O-bound, dan Isolate untuk operasi CPU-bound.

← Sebelumnya: Null Safety   Berikutnya: Fitur Dart 3 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact