MobX #
MobX adalah salah satu pustaka manajemen state di ekosistem Flutter yang menerapkan filosofi Transparent Functional Reactive Programming (TFRP). Pendekatan ini memungkinkan data aplikasi, antarmuka pengguna (UI), dan logika bisnis terhubung secara otomatis dan dinamis melalui mekanisme observasi yang tidak kasatmata. Awalnya dikembangkan di ekosistem JavaScript dan sangat populer di komunitas React, MobX di-porting ke bahasa Dart dengan tetap mempertahankan prinsip utamanya: apa saja yang dapat diturunkan secara otomatis dari application state, harus diturunkan secara otomatis.
Bagi kita yang ingin membangun aplikasi dengan reaktivitas tinggi tanpa harus menulis banyak kode boilerplate atau mengelola pembaruan UI secara manual, MobX menawarkan solusi yang sangat elegan. Dalam dokumen ini, kita akan membedah konsep dasar MobX, memahami cara kerja pilarnya, mempelajari praktik terbaik, hingga mengimplementasikannya dalam skenario dunia nyata.
Konsep Dasar Transparent Functional Reactive Programming (TFRP) #
Sebelum kita masuk ke implementasi teknis, penting untuk memahami paradigma di balik MobX. Transparent Functional Reactive Programming (TFRP) adalah bentuk pemrograman reaktif di mana kita tidak perlu berlangganan (subscribe) secara eksplisit ke suatu aliran data (stream atau observable). MobX secara transparan mendeteksi variabel mana saja yang dibaca selama eksekusi sebuah fungsi, dan secara otomatis menetapkan relasi dependensi.
Di pustaka manajemen state lain seperti Bloc atau bahkan Provider, kita sering kali harus mendefinisikan secara manual bagian mana yang memicu perubahan dan widget mana yang harus mendengarkan perubahan tersebut. Dengan TFRP, MobX melakukan pelacakan dependensi ini di latar belakang. Ketika sebuah variabel observable berubah, seluruh perhitungan (computed) atau widget yang menggunakan variabel tersebut akan langsung diperbarui. Ini menghilangkan kesalahan manusia (human error) seperti lupa memperbarui UI atau melakukan pembaruan UI yang berlebihan (over-rendering).
Tiga Pilar Utama MobX #
Arsitektur MobX bersandar pada tiga konsep utama yang saling terhubung membentuk siklus satu arah (uni-directional data flow). Ketiga konsep tersebut adalah Observables, Actions, dan Reactions. Selain itu, terdapat Computed Properties yang bertindak sebagai jembatan cerdas antara Observables dan Reactions.
Berikut adalah diagram alir kerja MobX yang menggambarkan bagaimana aksi memicu perubahan state, yang kemudian memperbarui UI dan menjalankan efek samping secara otomatis:
graph TD
classDef default stroke:#333,stroke-width:2px;
A["Actions (Metode Store)"] -->|Mengubah| B["Observables (State Reaktif)"]
B -->|Memicu Recomputasi| C["Computed Properties (State Turunan)"]
B -->|Memicu Efek Samping| D["Reactions (Side Effects & UI Rebuild)"]
C -->|Digunakan oleh| D
D -->|Mengirim Event / Interaksi| AMari kita bedah masing-masing komponen pilar tersebut secara mendalam:
- Observables (State): Observables mewakili state atau data mentah dari aplikasi kita yang dapat berubah seiring waktu. Setiap perubahan pada nilai observable akan dicatat oleh MobX untuk kemudian diberitahukan ke semua konsumen yang membutuhkannya.
- Actions (Aksi): Actions adalah metode atau fungsi yang bertugas mengubah nilai dari variabel observable. Di MobX, semua mutasi state harus terjadi di dalam Action untuk memastikan pelacakan perubahan yang teratur dan batching pembaruan UI yang efisien.
- Computed Properties (State Turunan): Computed properties adalah nilai yang diturunkan dari observable lain. Nilai ini di-cache secara otomatis (memoize) dan hanya dihitung ulang jika observable yang menjadi dasarnya mengalami perubahan nilai.
- Reactions (Efek Samping): Reactions mirip dengan computed properties, namun alih-alih menghasilkan nilai baru, reactions menghasilkan efek samping (side effects) seperti mencetak log, mengirim data ke API, menyimpan data ke lokal, atau membangun ulang widget di layar (melalui
Observer).
Instalasi dan Konfigurasi Proyek #
Untuk menggunakan MobX di Flutter, kita memerlukan beberapa pustaka di dalam berkas pubspec.yaml. Karena MobX menggunakan fitur code generation untuk mengurangi penulisan kode boilerplate yang berulang, kita memerlukan beberapa pustaka di bagian dependencies dan dev_dependencies.
Tambahkan baris berikut ke berkas konfigurasi pubspec.yaml proyek kita:
dependencies:
flutter:
sdk: flutter
# Pustaka utama MobX untuk Dart
mobx: ^2.5.0
# Integrasi MobX dengan ekosistem widget Flutter
flutter_mobx: ^2.3.0
dev_dependencies:
flutter_test:
sdk: flutter
# Alat untuk menjalankan generator kode di Dart
build_runner: ^2.9.0
# Generator kode spesifik untuk kelas Store MobX
mobx_codegen: ^2.7.4
Setelah menambahkan dependensi tersebut, jalankan perintah berikut di terminal proyek kita untuk mengunduh pustaka:
rtk flutter pub get
Membuat Store Pertama Kita #
Store dalam MobX adalah sebuah kelas kontainer tempat kita menaruh semua variabel observable, computed properties, dan action yang saling berkaitan. Untuk meminimalkan boilerplate, kita menggunakan anotasi generator dari mobx_codegen.
Mari kita buat store sederhana untuk menghitung angka (counter) guna memahami struktur berkas dasar MobX:
// counter_store.dart
import 'package:mobx/mobx.dart';
// Wajib: Menghubungkan berkas ini dengan berkas hasil generate
part 'counter_store.g.dart';
// Kelas utama yang diakses oleh UI.
// Merupakan gabungan dari abstract class dan mixin hasil generate.
class CounterStore = _CounterStore with _$CounterStore;
// Abstract class tempat kita menulis logika bisnis utama.
abstract class _CounterStore with Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
@action
void decrement() {
value--;
}
@action
void reset() {
value = 0;
}
}
Setelah menulis kode di atas, analyzer Dart akan memunculkan pesan error karena berkas counter_store.g.dart belum ada. Kita harus menjalankan generator kode untuk membuatnya.
Menjalankan build_runner #
Buka terminal di direktori proyek kita dan jalankan perintah berikut:
# Menjalankan build sekali saja
rtk flutter pub run build_runner build --delete-conflicting-outputs
Jika kita sedang dalam tahap pengembangan aktif, menjalankan perintah di atas secara terus-menerus bisa sangat melelahkan. Kita dapat menggunakan mode watch agar generator otomatis berjalan setiap kali ada perubahan pada berkas store:
# Menjalankan build secara real-time setiap kali berkas disimpan
rtk flutter pub run build_runner watch --delete-conflicting-outputs
Perintah di atas akan menghasilkan berkas counter_store.g.dart yang berisi semua implementasi reaktivitas di belakang layar, sehingga kode utama kita tetap bersih dan mudah dibaca.
Observables: State Reaktif #
Anotasi @observable memberi tahu generator MobX untuk membuat variabel tersebut reaktif. MobX akan mengawasi kapan pun variabel ini dibaca dan kapan pun nilainya berubah.
Tipe Data Primitif vs Koleksi #
Untuk tipe data primitif seperti int, double, String, dan bool, kita bisa langsung menggunakannya seperti biasa. Namun, untuk tipe data koleksi seperti List, Map, dan Set, kita tidak boleh menggunakan tipe data bawaan Dart secara langsung jika kita ingin melacak perubahan di dalam koleksi tersebut.
Perhatikan contoh kesalahan umum berikut:
// SALAH: Perubahan elemen di dalam list tidak akan terdeteksi
@observable
List<String> daftarTugas = [];
// Di bagian action:
void tambahTugas(String tugas) {
// Alamat memori list tidak berubah, MobX tidak mendeteksi mutasi elemen!
daftarTugas.add(tugas);
}
Untuk mengatasi masalah reaktivitas pada koleksi, MobX menyediakan tipe koleksi reaktif khusus yaitu ObservableList, ObservableMap, dan ObservableSet:
// BENAR: Gunakan tipe data koleksi dari MobX
@observable
ObservableList<String> daftarTugas = ObservableList<String>();
// Di bagian action:
@action
void tambahTugas(String tugas) {
// Setiap penambahan atau penghapusan elemen akan memicu rebuild UI secara otomatis
daftarTugas.add(tugas);
}
Anotasi Readonly #
Terkadang kita ingin sebuah variabel observable dapat dibaca oleh UI tetapi hanya boleh diubah dari dalam store itu sendiri. MobX menyediakan anotasi @readonly untuk skenario ini:
abstract class _AuthStore with Store {
// Menghasilkan getter publik namun setter bersifat privat untuk store
@readonly
String? _token;
@action
void setToken(String baru) {
_token = baru;
}
}
Dengan menggunakan @readonly, kita mencegah widget luar memodifikasi state secara tidak sengaja di luar action yang sudah ditentukan.
Actions: Mengubah State Secara Terkontrol #
Actions adalah metode yang bertugas memodifikasi state. Mengapa kita harus menggunakan action? Mengapa tidak mengubah nilai variabel observable langsung dari UI? Ada beberapa alasan penting:
- Batching Update: Jika sebuah action mengubah lima variabel observable yang berbeda, MobX akan menahan notifikasi perubahan ke UI hingga seluruh isi action selesai dieksekusi. UI hanya akan melakukan rebuild satu kali, bukan lima kali. Ini meningkatkan performa rendering aplikasi secara signifikan.
- Keterbacaan & Debugging: Dengan melacak mutasi hanya di dalam action, kita dapat dengan mudah menelusuri dari mana perubahan state tersebut berasal.
- Pemberlakuan Mutasi Terpusat: Kita dapat mengonfigurasi MobX agar melarang mutasi observable di luar action (opsi
enforceActions).
Action Asinkron (Async Actions) #
Aplikasi nyata penuh dengan operasi asinkron seperti pemanggilan API jaringan atau pembacaan basis data lokal. Menulis action asinkron di MobX memerlukan perhatian khusus karena setelah kata kunci await, jalannya eksekusi kode berada di luar konteks action yang asli.
Berikut adalah cara yang aman dan direkomendasikan untuk menulis action asinkron:
abstract class _UserStore with Store {
@observable
bool isLoading = false;
@observable
String? username;
@observable
String? errorMessage;
@action
Future<void> fetchUser(int id) async {
isLoading = true;
errorMessage = null;
try {
// Operasi I/O asinkron dilakukan di luar pemblokiran
final result = await apiService.getUserName(id);
// Mengubah state setelah await harus dilakukan dengan aman.
// Kita bisa langsung menetapkannya jika menggunakan versi MobX terbaru,
// atau membungkusnya dalam 'runInAction' jika mode enforceActions diaktifkan secara ketat.
runInAction(() {
username = result;
isLoading = false;
});
} catch (e) {
runInAction(() {
errorMessage = e.toString();
isLoading = false;
});
}
}
}
Menggunakan runInAction memastikan bahwa mutasi state yang terjadi setelah operasi asinkron selesai tetap dianggap berada dalam konteks action, sehingga aturan batching dan pelacakan tetap berjalan dengan baik.
Computed Properties: State Turunan #
Computed properties ditandai dengan anotasi @computed pada metode getter. Konsep computed sangat penting untuk menjaga store kita tetap bersih dari redundansi data. Nilai computed dihitung dari observable lain dan hasilnya akan disimpan di memori (cache).
Keuntungan Memoization #
Jika kita memanggil computed property berulang kali di widget yang berbeda, MobX tidak akan menghitung ulang rumusnya. MobX hanya mengembalikan nilai yang sudah di-cache. Perhitungan ulang baru akan dijalankan saat salah satu observable yang digunakan di dalam rumusnya mengalami perubahan.
abstract class _KeranjangStore with Store {
@observable
ObservableList<ItemKeranjang> items = ObservableList();
// Computed property pertama
@computed
double get subtotal => items.fold(0, (sum, item) => sum + item.hargaTotal);
// Computed property kedua yang bergantung pada computed property pertama
@computed
double get pajak => subtotal * 0.11;
// Computed property ketiga yang menggabungkan beberapa nilai
@computed
double get totalBayar => subtotal + pajak;
@computed
bool get kosong => items.isEmpty;
}
Dengan struktur seperti ini, kita tidak perlu memelihara variabel subtotal, pajak, dan totalBayar secara manual di setiap action tambah atau hapus item. Cukup kelola daftar items di dalam action, dan sisanya akan diselesaikan secara otomatis oleh computed properties.
Reactions: Mengelola Efek Samping #
Reactions digunakan ketika kita ingin menjalankan logika non-UI (side effects) sebagai tanggapan dari perubahan observable. Ada tiga jenis reaction yang disediakan oleh MobX: autorun, reaction, dan when.
1. autorun #
autorun akan langsung berjalan sekali saat pertama kali didefinisikan, lalu berjalan kembali setiap kali ada variabel observable di dalamnya yang berubah nilai.
final disposer = autorun((_) {
print('Jumlah saat ini adalah ${store.value}');
});
2. reaction #
Berbeda dengan autorun, reaction tidak langsung berjalan saat didefinisikan. Ia menerima dua fungsi parameter: fungsi pertama untuk mendeteksi data yang dipantau (tracker), dan fungsi kedua sebagai efek samping yang dieksekusi saat data pantauan berubah.
final disposer = reaction(
(_) => store.isLoggedIn, // Memantau perubahan status login
(bool loggedIn) {
if (loggedIn) {
navigationService.goToHome();
} else {
navigationService.goToLogin();
}
},
);
3. when #
when hanya berjalan satu kali. Ia memantau kondisi tertentu (fungsi pertama harus mengembalikan nilai boolean), dan ketika kondisi tersebut bernilai true, ia menjalankan fungsi efek samping (fungsi kedua) lalu langsung mematikan dirinya sendiri (auto-dispose).
final disposer = when(
(_) => store.downloadProgress == 100, // Menunggu kondisi terpenuhi
() => notificationService.showComplete(), // Jalankan sekali
);
Penting: Membersihkan Reactions (Cleanup) #
Setiap kali kita membuat autorun, reaction, atau when, fungsi tersebut akan mengembalikan objek berjenis ReactionDisposer. Kita wajib menyimpan disposer ini dan memanggilnya saat store atau widget dihancurkan untuk mencegah kebocoran memori (memory leak).
class HalamanDetail extends StatefulWidget {
const HalamanDetail({super.key});
@override
State<HalamanDetail> createState() => _HalamanDetailState();
}
class _HalamanDetailState extends State<HalamanDetail> {
late ReactionDisposer _disposer;
final _store = DetailStore();
@override
void initState() {
super.initState();
// Setup reaction untuk memantau error pesan
_disposer = reaction(
(_) => _store.errorMessage,
(String? msg) {
if (msg != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
},
);
}
@override
void dispose() {
_disposer(); // Mematikan reaction untuk mencegah memory leak
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
);
}
}
Observer Widget: Sinkronisasi UI Otomatis #
Untuk menghubungkan store MobX dengan antarmuka pengguna Flutter, kita menggunakan widget Observer yang disediakan oleh pustaka flutter_mobx.
Widget Observer akan secara otomatis melacak observable apa saja yang dibaca di dalam parameter builder-nya. Ketika salah satu dari observable tersebut berubah, Observer akan memicu rebuild secara instan pada widget tersebut saja.
Tips Rebuild Granular #
Kunci performa tinggi dalam menggunakan MobX adalah menjaga cakupan widget Observer sekecil mungkin. Hindari membungkus seluruh halaman dengan satu widget Observer besar jika hanya sebagian kecil elemen saja yang membutuhkan reaktivitas.
Mari kita bandingkan dua pola berikut:
// POLA KELIRU: Rebuild makro yang tidak efisien
Observer(
builder: (context) {
return Scaffold(
appBar: AppBar(title: Text('Profil ${store.username}')),
body: Column(
children: [
const WidgetPetaDuniaBesar(), // Widget statis berat ikut rebuild!
Text('Skor Game: ${store.score}'),
],
),
);
},
)
// POLA BENAR: Rebuild granular yang efisien
Scaffold(
appBar: AppBar(
title: Observer(builder: (_) => Text('Profil ${store.username}')),
),
body: Column(
children: [
const WidgetPetaDuniaBesar(), // Aman, tidak akan rebuild karena di luar Observer
Observer(builder: (_) => Text('Skor Game: ${store.score}')),
],
),
)
Dengan mengisolasi Observer pada komponen teks skor saja, kita menghemat daya komputasi perangkat karena widget statis yang berat seperti peta dunia tidak perlu ikut dibangun ulang saat skor pemain bertambah.
Studi Kasus: Implementasi Toko Online Modern #
Mari kita gabungkan semua pemahaman di atas ke dalam sebuah skenario nyata aplikasi toko online. Kita akan membuat model item belanjaan, store untuk mengelola daftar belanja, dan tampilan antarmuka yang reaktif.
1. Model Data #
// item_keranjang.dart
class ItemKeranjang {
final String id;
final String nama;
final double harga;
ItemKeranjang({
required this.id,
required this.nama,
required this.harga,
});
}
2. Implementasi Store Keranjang Belanja #
// keranjang_store.dart
import 'package:mobx/mobx.dart';
import 'item_keranjang.dart';
part 'keranjang_store.g.dart';
class KeranjangStore = _KeranjangStore with _$KeranjangStore;
abstract class _KeranjangStore with Store {
// Menggunakan ObservableList agar penambahan/penghapusan elemen terdeteksi
@observable
ObservableList<ItemKeranjang> daftarBelanja = ObservableList<ItemKeranjang>();
@observable
bool sedangMemproses = false;
@observable
String? catatanTambahan;
// Computed state untuk total harga
@computed
double get totalHarga => daftarBelanja.fold(0.0, (sum, item) => sum + item.harga);
// Computed state untuk jumlah barang unik
@computed
int get jumlahBarang => daftarBelanja.length;
// Computed state untuk mengecek apakah keranjang kosong
@computed
bool get apakahKosong => daftarBelanja.isEmpty;
@action
void tambahItem(ItemKeranjang item) {
daftarBelanja.add(item);
}
@action
void hapusItem(ItemKeranjang item) {
daftarBelanja.remove(item);
}
@action
void setCatatan(String teks) {
catatanTambahan = teks;
}
@action
Future<void> prosesCheckout() async {
if (apakahKosong) return;
sedangMemproses = true;
try {
// Menyimulasikan pengiriman data ke server API
await Future.delayed(const Duration(seconds: 2));
daftarBelanja.clear();
catatanTambahan = null;
} finally {
sedangMemproses = false;
}
}
}
3. Pembuatan Antarmuka UI Flutter #
// halaman_keranjang.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'keranjang_store.dart';
import 'item_keranjang.dart';
class HalamanKeranjang extends StatelessWidget {
final KeranjangStore store;
const HalamanKeranjang({super.key, required this.store});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Keranjang Belanja Kita'),
),
body: Column(
children: [
Expanded(
child: Observer(
builder: (_) {
if (store.apakahKosong) {
return const Center(child: Text('Keranjang Anda kosong.'));
}
return ListView.builder(
itemCount: store.jumlahBarang,
itemBuilder: (context, index) {
final item = store.daftarBelanja[index];
return ListTile(
title: Text(item.nama),
subtitle: Text('Rp ${item.harga.toStringAsFixed(0)}'),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => store.hapusItem(item),
),
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Catatan Belanja',
border: OutlineInputBorder(),
),
onChanged: store.setCatatan,
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total Pembayaran:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Observer(
builder: (_) => Text(
'Rp ${store.totalHarga.toStringAsFixed(0)}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
),
],
),
const SizedBox(height: 16),
Observer(
builder: (_) {
return ElevatedButton(
onPressed: (store.apakahKosong || store.sedangMemproses)
? null
: () => store.prosesCheckout(),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: store.sedangMemproses
? const CircularProgressIndicator()
: const Text('Proses Checkout'),
);
},
),
],
),
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final idUnik = DateTime.now().millisecondsSinceEpoch.toString();
store.tambahItem(
ItemKeranjang(
id: idUnik,
nama: 'Barang #$idUnik',
harga: 25000.0,
),
);
},
child: const Icon(Icons.add_shopping_cart),
),
);
}
}
Integrasi MobX dengan Provider untuk Dependency Injection #
Meskipun MobX mengelola state reaktif dengan sangat baik, MobX tidak menyediakan mekanisme untuk membagikan instansi store tersebut ke seluruh pohon widget (widget tree). Untuk mengatasi hal ini, kita menggabungkan MobX dengan pustaka provider sebagai penyedia layanan Dependency Injection (DI).
Berikut adalah cara mengintegrasikan MobX dan Provider di file masuk utama (main.dart):
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'keranjang_store.dart';
import 'halaman_keranjang.dart';
void main() {
runApp(
MultiProvider(
providers: [
// Mendaftarkan store MobX agar bisa diakses oleh widget di bawahnya
Provider<KeranjangStore>(
create: (_) => KeranjangStore(),
),
],
child: const AplikasiKita(),
),
);
}
class AplikasiKita extends StatelessWidget {
const AplikasiKita({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Toko Reaktif Kita',
theme: ThemeData(primarySwatch: Colors.blue),
home: Consumer<KeranjangStore>(
builder: (context, store, _) {
return HalamanKeranjang(store: store);
},
),
);
}
}
Jika kita ingin mengakses store dari widget terdalam tanpa menggunakan Consumer, kita dapat membacanya langsung menggunakan context.read():
class TombolTambahCepat extends StatelessWidget {
const TombolTambahCepat({super.key});
@override
Widget build(BuildContext context) {
// Membaca instansi store tanpa berlangganan pembaruan (karena action tidak merebuild widget ini)
final store = context.read<KeranjangStore>();
return ElevatedButton(
onPressed: () {
store.tambahItem(
ItemKeranjang(
id: 'cepat',
nama: 'Produk Cepat',
harga: 15000.0,
),
);
},
child: const Text('Tambah Cepat'),
);
}
}
Menguji (Testing) Store MobX #
Salah satu keuntungan terbesar memisahkan logika dari UI menggunakan store MobX adalah kemudahan dalam menulis unit test. Karena store tidak bergantung pada context Flutter maupun widget tree, kita dapat menguji perilakunya menggunakan Dart Unit Test biasa secara cepat.
Berikut adalah berkas pengujian untuk KeranjangStore kita:
// test/keranjang_store_test.dart
import 'package:flutter_test/flutter_test.dart';
import '../lib/keranjang_store.dart';
import '../lib/item_keranjang.dart';
void main() {
group('Pengujian Logika Bisnis KeranjangStore', () {
late KeranjangStore store;
setUp(() {
store = KeranjangStore();
});
test('Keranjang harus dimulai dengan keadaan kosong', () {
expect(store.apakahKosong, isTrue);
expect(store.jumlahBarang, equals(0));
expect(store.totalHarga, equals(0.0));
});
test('Menambah item harus memperbarui list dan total harga', () {
final item = ItemKeranjang(id: '1', nama: 'Buku Dart', harga: 50000.0);
store.tambahItem(item);
expect(store.apakahKosong, isFalse);
expect(store.jumlahBarang, equals(1));
expect(store.totalHarga, equals(50000.0));
expect(store.daftarBelanja.first.nama, equals('Buku Dart'));
});
test('Menghapus item harus memperbarui list dan total harga', () {
final item1 = ItemKeranjang(id: '1', nama: 'Buku Dart', harga: 50000.0);
final item2 = ItemKeranjang(id: '2', nama: 'Kopi Susu', harga: 20000.0);
store.tambahItem(item1);
store.tambahItem(item2);
store.hapusItem(item1);
expect(store.jumlahBarang, equals(1));
expect(store.totalHarga, equals(20000.0));
expect(store.daftarBelanja.first.id, equals('2'));
});
test('Proses checkout harus mengosongkan keranjang belanja', () async {
final item = ItemKeranjang(id: '1', nama: 'Buku Dart', harga: 50000.0);
store.tambahItem(item);
// Jalankan aksi asinkron
final future = store.prosesCheckout();
// Selama proses berjalan, state sedangMemproses harus bernilai true
expect(store.sedangMemproses, isTrue);
await future;
// Setelah selesai, state kembali normal dan keranjang kosong
expect(store.sedangMemproses, isFalse);
expect(store.apakahKosong, isTrue);
});
});
}
Dengan cakupan pengujian seperti ini, kita dapat memastikan kebenaran logika bisnis aplikasi kita dengan sangat cepat tanpa perlu merender tampilan apa pun di simulator.
Anti-Pattern MobX yang Harus Dihindari #
Agar aplikasi kita tetap memiliki performa optimal dan bebas dari bug yang sulit dilacak, pastikan untuk menghindari beberapa kebiasaan buruk berikut:
- Mengubah Variabel Observable Secara Langsung Tanpa Action: Walaupun MobX Dart secara default masih memperbolehkannya jika tidak dikonfigurasi ketat, memodifikasi observable di luar action merusak konsep arsitektur satu arah dan membuat pelacakan mutasi menjadi sulit. Selalu bungkus setiap perubahan data di dalam
@actionataurunInAction. - Lupa Mematikan Reactions: Setiap kali kita memanggil
autorun,reaction, atauwhendi dalam widget stateful, simpan hasilnya dan panggil disposer di methoddispose(). Mengabaikan ini akan menyebabkan kebocoran memori (memory leak) karena reaction terus berjalan di latar belakang mendengarkan perubahan state. - Menggunakan List Bawaan Dart Secara Langsung: Selalu gunakan
ObservableListatau konversikan denganObservableList.of()jika kita membutuhkan array reaktif. List biasa tidak memicu perubahan saat elemennya ditambahkan atau dikurangi. - Terlalu Banyak Logika di Computed Property: Ingat bahwa computed properties ditujukan untuk data turunan yang ringan. Jangan melakukan panggilan jaringan API, query database, atau operasi I/O berat lainnya di dalam metode getter
@computed.
Ringkasan #
- TFRP (Transparent Functional Reactive Programming) menghubungkan perubahan data dengan antarmuka UI secara otomatis tanpa perlu manajemen subscription secara eksplisit.
- Tiga Pilar MobX terdiri dari Observables (data reaktif), Actions (pengubah data), dan Reactions (efek samping perubahan data).
- Computed Properties bertindak sebagai state turunan yang di-cache secara efisien (memoized) untuk menghindari kalkulasi berulang yang tidak diperlukan.
- Koleksi Reaktif wajib menggunakan kelas khusus seperti
ObservableListdanObservableMapagar perubahan data di dalamnya terdeteksi sempurna oleh UI.- Observer Widget harus dibuat sespesifik mungkin (granular) untuk menghindari pembangunan ulang widget statis yang tidak perlu demi performa rendering terbaik.
- Reaction Disposer harus dipanggil ketika widget dihancurkan (
dispose()) guna mencegah penumpukan resource memori di latar belakang.- Provider dapat diintegrasikan sebagai Dependency Injection (DI) yang bersih untuk menyalurkan instansi store MobX ke sub-widget tree.
- Unit Testing pada store MobX sangat mudah dilakukan secara mandiri karena logika bisnis terpisah sepenuhnya dari framework UI Flutter.
← Sebelumnya: Bloc & Cubit Berikutnya: Perbandingan & Kapan Memilih →