Ada satu bug yang paling tidak nyaman untuk dijelaskan ke klien: UI yang tiba-tiba beku beberapa detik, lalu kembali normal seolah tidak terjadi apa-apa. Tidak ada pesan error, tidak ada crash report. Hanya layar yang tidak merespons selama beberapa detik yang terasa sangat panjang.
Penyebabnya hampir selalu sama: kode berat yang berjalan di main thread.
Di Flutter, semua widget rendering, animasi, dan respons gesture berjalan di satu thread yang disebut UI thread. Kalau Anda menjalankan parsing JSON besar, enkripsi file, atau kompresi gambar di thread yang sama, Flutter tidak punya kesempatan untuk menggambar frame berikutnya sampai operasi itu selesai. Hasilnya: frame drop, UI beku, pengguna frustrasi.
Solusinya adalah Dart Isolate β mekanisme untuk menjalankan kode di thread terpisah yang benar-benar tidak memblokir UI thread.
Dart Tidak Punya Thread Biasa β Ini Perbedaannya
Kalau Anda punya latar belakang Java atau Kotlin, konsep thread sudah familiar. Beberapa thread berjalan bersamaan, berbagi memori, dan Anda harus sangat hati-hati dengan race condition dan synchronization.
Dart mengambil pendekatan yang berbeda. Sebagai ganti thread dengan shared memory, Dart menggunakan Isolate: unit eksekusi yang benar-benar terisolasi, masing-masing dengan heap memorinya sendiri. Komunikasi antar isolate hanya bisa dilakukan melalui message passing β tidak ada shared state, tidak ada race condition.
Tradeoffnya: tidak bisa langsung mengoper object kompleks antar isolate (hanya tipe primitif, List, Map, dan beberapa tipe khusus yang bisa di-transfer). Tapi untuk kebanyakan use case β parsing data besar, komputasi berat, operasi file β ini bukan masalah.
compute(): Isolate untuk 90% Kasus
Flutter menyediakan fungsi compute() yang membungkus kompleksitas pembuatan Isolate dalam satu pemanggilan fungsi:
// Fungsi yang akan dijalankan di isolate terpisah
// HARUS top-level function atau static method β bukan lambda atau closure
List<Artikel> parseArtikelJson(String jsonString) {
final List<dynamic> data = jsonDecode(jsonString);
return data.map((item) => Artikel.fromJson(item)).toList();
}
// Penggunaan di widget/provider
Future<List<Artikel>> ambilArtikel() async {
final response = await http.get(Uri.parse('https://api.contoh.com/artikel'));
// Parsing JSON berjalan di isolate terpisah β UI tetap responsif
final artikelList = await compute(parseArtikelJson, response.body);
return artikelList;
}
Batas terpenting: fungsi yang dioper ke compute() harus berupa top-level function atau static method. Ini karena isolate tidak bisa mengakses closure dari isolate lain β ia tidak bisa berbagi memori dengan isolate pemanggil.
Untuk operasi yang butuh data besar sebagai input dan menghasilkan output yang besar, compute() sudah lebih dari cukup.
Kapan compute() Tidak Cukup
Ada dua situasi di mana compute() kurang ideal:
Situasi 1: Operasi yang perlu mengirim progress update. compute() hanya mengembalikan satu nilai di akhir. Kalau Anda memproses 10.000 baris data dan ingin menampilkan progress bar yang akurat, Anda butuh komunikasi dua arah.
Situasi 2: Isolate yang perlu hidup lama dan menerima banyak task. Membuat isolate baru untuk setiap operasi ada overhead-nya. Kalau Anda melakukan ratusan operasi kecil per menit, overhead pembuatan isolate bisa lebih besar dari pekerjaan sebenarnya.
Untuk kedua situasi ini, Anda perlu membuat isolate secara manual dengan ReceivePort dan SendPort.
Isolate Manual dengan Progress Reporting
// Kelas untuk membungkus komunikasi dengan isolate
class IsolateProcessor {
late Isolate _isolate;
late ReceivePort _receivePort;
late SendPort _sendPort;
// Stream untuk menerima update dari isolate
Stream<dynamic> get updates => _receivePort.asBroadcastStream();
Future<void> inisialisasi() async {
_receivePort = ReceivePort();
_isolate = await Isolate.spawn(
_entryPoint,
_receivePort.sendPort,
);
// Tunggu isolate mengirim SendPort-nya
_sendPort = await _receivePort.first as SendPort;
}
void kirimTask(Map<String, dynamic> task) {
_sendPort.send(task);
}
void tutup() {
_isolate.kill();
_receivePort.close();
}
// Entry point isolate β harus top-level function
static void _entryPoint(SendPort sendPortKePemanggil) {
final receivePort = ReceivePort();
// Kirim SendPort ke isolate pemanggil agar bisa menerima task
sendPortKePemanggil.send(receivePort.sendPort);
receivePort.listen((message) {
final task = message as Map<String, dynamic>;
final items = task['data'] as List;
final total = items.length;
// Proses dengan progress update
for (int i = 0; i < items.length; i++) {
// ... proses item[i] ...
// Kirim progress setiap 10%
if (i % (total ~/ 10) == 0) {
sendPortKePemanggil.send({
'tipe': 'progress',
'nilai': i / total,
});
}
}
sendPortKePemanggil.send({'tipe': 'selesai', 'hasil': '...' });
});
}
}
IsolateNameServer: Komunikasi Antar Isolate Berdasarkan Nama
Dart menyediakan IsolateNameServer yang memungkinkan isolate mendaftarkan dirinya dengan nama string, sehingga isolate lain bisa menemukannya tanpa harus meneruskan SendPort secara manual.
// Di background isolate:
IsolateNameServer.registerPortWithName(
receivePort.sendPort,
'background-processor',
);
// Di isolate lain yang ingin berkomunikasi:
final port = IsolateNameServer.lookupPortByName('background-processor');
port?.send('pesan dari isolate lain');
Ini sangat berguna dalam aplikasi yang menggunakan background service atau flutter_background_service, di mana komunikasi antara foreground dan background isolate perlu dijaga.
Kasus Nyata: Mengompresi Gambar di Background
Salah satu use case paling umum di aplikasi saya adalah kompresi gambar sebelum upload. Tanpa isolate, proses ini membekukan UI selama 1β3 detik tergantung ukuran gambar.
// Top-level function untuk kompresi
Uint8List kompresGambar(Map<String, dynamic> params) {
final bytes = params['bytes'] as Uint8List;
final kualitas = params['kualitas'] as int;
final image = img.decodeImage(bytes)!;
// Resize jika terlalu besar
final imageResized = image.width > 1920
? img.copyResize(image, width: 1920)
: image;
return Uint8List.fromList(
img.encodeJpg(imageResized, quality: kualitas),
);
}
// Penggunaan
Future<Uint8List> prosesGambar(Uint8List bytesAsli) async {
return compute(kompresGambar, {
'bytes': bytesAsli,
'kualitas': 85,
});
}
Dengan ini, pengguna bisa tetap scroll, mengetik, atau berinteraksi dengan UI lain selama gambar diproses di background.
Kesimpulan
Dart Isolate bukan fitur eksotis untuk skenario khusus β ini adalah alat yang perlu ada di toolkit setiap developer Flutter yang serius. Kapan pun Anda punya operasi yang membutuhkan waktu lebih dari beberapa milidetik, pertimbangkan untuk memindahkannya ke isolate.
Mulai dari compute() untuk kasus sederhana. Pindah ke isolate manual hanya ketika memang butuh komunikasi dua arah atau isolate yang persisten. Dan selalu ukur sebelum dan sesudah dengan Flutter DevTools β optimasi tanpa pengukuran hanya tebakan.
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar