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/awaituntuk 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
StreamBuilderuntuk memperbarui UI secara reaktif.- Isolate memberikan parallelism sejati memanfaatkan multiple CPU core — gunakan
compute()atauIsolate.run()untuk operasi CPU-intensive yang bisa menyebabkan jank.- Gunakan Future/Stream untuk operasi I/O-bound, dan Isolate untuk operasi CPU-bound.