MobX #
MobX adalah library state management berbasis Transparent Functional Reactive Programming (TFRP) — sebuah pendekatan di mana state, UI, dan logika bisnis dihubungkan secara otomatis melalui mekanisme observasi. MobX berasal dari ekosistem JavaScript/React dan di-port ke Dart dengan filosofi yang sama: apa yang bisa diturunkan dari state, harus diturunkan secara otomatis.
Tiga Konsep Inti MobX #
OBSERVABLE → Reactive state (data yang bisa berubah dan dipantau)
↓
ACTION → Operasi yang mengubah Observable
↓
REACTION → Efek samping yang berjalan otomatis ketika Observable berubah
(termasuk rebuild UI melalui Observer widget)
COMPUTED → State turunan yang dihitung otomatis dari Observable lain
Instalasi #
# pubspec.yaml
dependencies:
mobx: ^2.5.0
flutter_mobx: ^2.3.0
dev_dependencies:
build_runner: ^2.9.0
mobx_codegen: ^2.7.4
Store — Wadah State MobX #
Store adalah kelas yang mengumpulkan Observable, Action, dan Computed yang saling berkaitan. MobX menggunakan code generation untuk menghilangkan boilerplate.
Struktur Dasar Store #
// counter_store.dart
import 'package:mobx/mobx.dart';
// Wajib: sertakan file generated
part 'counter_store.g.dart';
// Deklarasi: kelas publik = abstract class + mixin generated
class CounterStore = _CounterStore with _$CounterStore;
// Implementasi di abstract class dengan Store mixin
abstract class _CounterStore with Store {
@observable
int value = 0;
@action
void increment() => value++;
@action
void decrement() => value--;
@action
void reset() => value = 0;
}
Jalankan Code Generation #
# Jalankan sekali untuk generate file .g.dart
flutter pub run build_runner build --delete-conflicting-outputs
# Atau watch mode -- otomatis regenerate saat file berubah (selama development)
flutter pub run build_runner watch --delete-conflicting-outputs
Perintah ini menghasilkan counter_store.g.dart yang berisi semua boilerplate reaktivitas.
Observable — State yang Dipantau #
@observable menandai field sebagai reactive state — MobX akan melacak siapa yang menggunakannya dan memberitahu mereka saat nilainya berubah:
abstract class _ProdukStore with Store {
// Observable primitif
@observable
bool isLoading = false;
@observable
String? errorMessage;
// Observable list -- gunakan ObservableList, bukan List biasa!
@observable
ObservableList<Produk> produk = ObservableList<Produk>();
// Observable map
@observable
ObservableMap<String, int> stok = ObservableMap<String, int>();
// Observable dari tipe kustom
@observable
Produk? produkTerpilih;
// @readonly: menghasilkan getter publik tapi field privat
// Observer dari luar tidak bisa mengubah nilai langsung
@readonly
String _status = 'idle';
}
Perbedaan ObservableList vs List Biasa #
// SALAH: List biasa -- MobX tidak melacak perubahan isi list
@observable
List<Produk> produk = []; // push/remove TIDAK di-track!
produk.add(item); // Observer tidak mendeteksi perubahan ini
// BENAR: ObservableList -- setiap mutasi di-track
@observable
ObservableList<Produk> produk = ObservableList();
produk.add(item); // Observer otomatis di-notify
produk.remove(item); // Observer otomatis di-notify
produk.clear(); // Observer otomatis di-notify
Action — Mutasi State #
@action menandai method yang mengubah Observable. Semua perubahan di dalam satu @action di-batch — Observer hanya di-notify sekali setelah action selesai, bukan setiap kali field berubah:
abstract class _ProdukStore with Store {
@observable
ObservableList<Produk> produk = ObservableList();
@observable
bool isLoading = false;
@observable
String? error;
// Action sinkron
@action
void setProdukTerpilih(Produk p) {
produkTerpilih = p;
}
// Action async
@action
Future<void> loadProduk() async {
isLoading = true; // perubahan 1
error = null; // perubahan 2
// Observer belum di-notify karena masih dalam action
try {
final data = await produkApi.getAll();
produk = ObservableList.of(data); // perubahan 3
} catch (e) {
error = e.toString(); // perubahan 3 (alternatif)
} finally {
isLoading = false; // perubahan 4
}
// Setelah action selesai: Observer di-notify sekali
}
// Action yang mengubah banyak field -- tetap di-batch
@action
void reset() {
produk.clear();
isLoading = false;
error = null;
produkTerpilih = null;
// Satu kali notifikasi, meskipun 4 field berubah
}
}
Computed — State Turunan #
@computed mendefinisikan state yang diturunkan dari Observable lain — nilainya dihitung ulang secara otomatis ketika Observable yang ia gunakan berubah. MobX mengikuti prinsip: apa yang bisa diturunkan, harus diturunkan secara otomatis:
abstract class _KeranjangStore with Store {
@observable
ObservableList<CartItem> items = ObservableList();
// Computed: otomatis update saat items berubah
@computed
double get totalHarga => items.fold(0, (sum, item) => sum + item.subtotal);
@computed
int get jumlahItem => items.fold(0, (sum, item) => sum + item.jumlah);
@computed
bool get isEmpty => items.isEmpty;
@computed
bool get isPremiumEligible => totalHarga >= 500000;
// Computed bisa bergantung pada computed lain
@computed
double get totalSetelahDiskon =>
isPremiumEligible ? totalHarga * 0.9 : totalHarga;
@computed
String get ringkasanKeranjang =>
'$jumlahItem item • Rp ${totalHarga.toStringAsFixed(0)}';
}
Berbeda dengan helper function biasa, @computed di-memoize — nilainya hanya dihitung ulang jika Observable yang digunakan benar-benar berubah.
Reaction — Side Effect Otomatis #
Reaction adalah cara MobX menjalankan kode secara otomatis ketika Observable berubah — di luar rebuild widget:
abstract class _AuthStore with Store {
@observable
User? currentUser;
// Simpan disposer untuk cleanup
late ReactionDisposer _persistenceDisposer;
late ReactionDisposer _logDisposer;
void setupReactions() {
// autorun: langsung berjalan dan setiap kali Observable yang digunakan berubah
_persistenceDisposer = autorun((_) {
if (currentUser != null) {
SharedPreferences.getInstance().then((prefs) {
prefs.setString('userId', currentUser!.id);
});
}
});
// reaction: track apa yang di-return, jalankan effect saat berubah
// Tidak langsung berjalan -- hanya setelah perubahan pertama
_logDisposer = reaction(
(_) => currentUser?.id, // apa yang di-track
(String? userId) { // effect saat berubah
print('User changed: $userId');
analyticsService.setUserId(userId);
},
);
// when: berjalan sekali saat kondisi terpenuhi, lalu auto-dispose
when(
(_) => currentUser != null,
() => print('User logged in: ${currentUser!.nama}'),
);
}
void dispose() {
_persistenceDisposer(); // panggil disposer untuk berhenti tracking
_logDisposer();
}
}
Observer Widget — Rebuild Otomatis #
Observer dari flutter_mobx adalah widget yang secara otomatis rebuild ketika Observable yang digunakan di dalam builder-nya berubah:
import 'package:flutter_mobx/flutter_mobx.dart';
class KonterWidget extends StatelessWidget {
final CounterStore store;
const KonterWidget({super.key, required this.store});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Observer hanya wrap bagian yang perlu reaktif
Observer(
builder: (_) => Text(
'${store.value}',
style: const TextStyle(fontSize: 48),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: store.decrement, // langsung assign action
child: const Icon(Icons.remove),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: store.increment,
child: const Icon(Icons.add),
),
],
),
],
);
}
}
Observer yang Granular #
Isolasi Observer sekecil mungkin untuk performa optimal:
// KURANG OPTIMAL: satu Observer besar
Observer(
builder: (_) => Column(
children: [
Text(store.nama), // rebuild jika nama ATAU status berubah
Text(store.status),
Text('${store.total}'),
],
),
)
// LEBIH BAIK: Observer terpisah untuk setiap bagian
Column(
children: [
Observer(builder: (_) => Text(store.nama)), // hanya rebuild saat nama berubah
Observer(builder: (_) => Text(store.status)), // hanya rebuild saat status berubah
Observer(builder: (_) => Text('${store.total}')), // hanya rebuild saat total berubah
],
)
Contoh Lengkap — Toko Produk #
// produk_store.dart
import 'package:mobx/mobx.dart';
part 'produk_store.g.dart';
class ProdukStore = _ProdukStore with _$ProdukStore;
abstract class _ProdukStore with Store {
final ProdukRepository _repository;
_ProdukStore(this._repository);
@observable
ObservableList<Produk> semuaProduk = ObservableList();
@observable
String _query = '';
@observable
bool isLoading = false;
@observable
String? error;
@computed
ObservableList<Produk> get produkTerfilter {
if (_query.isEmpty) return semuaProduk;
return ObservableList.of(
semuaProduk.where(
(p) => p.nama.toLowerCase().contains(_query.toLowerCase()),
),
);
}
@computed
bool get adaProduk => semuaProduk.isNotEmpty;
@action
void setQuery(String query) => _query = query;
@action
Future<void> loadProduk() async {
isLoading = true;
error = null;
try {
final data = await _repository.getAll();
semuaProduk = ObservableList.of(data);
} catch (e) {
error = e.toString();
} finally {
isLoading = false;
}
}
@action
Future<void> hapusProduk(String id) async {
final index = semuaProduk.indexWhere((p) => p.id == id);
if (index < 0) return;
// Optimistic update
final removed = semuaProduk.removeAt(index);
try {
await _repository.hapus(id);
} catch (e) {
// Rollback
semuaProduk.insert(index, removed);
error = 'Gagal menghapus: $e';
}
}
}
// produk_screen.dart
class ProdukScreen extends StatefulWidget {
const ProdukScreen({super.key});
@override
State<ProdukScreen> createState() => _ProdukScreenState();
}
class _ProdukScreenState extends State<ProdukScreen> {
// Store bisa dibuat di widget atau diinject via Provider
late final ProdukStore _store;
@override
void initState() {
super.initState();
_store = ProdukStore(context.read<ProdukRepository>());
_store.loadProduk();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Produk'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: Padding(
padding: const EdgeInsets.all(8),
child: TextField(
onChanged: _store.setQuery, // langsung sambungkan ke action
decoration: const InputDecoration(
hintText: 'Cari produk...',
prefixIcon: Icon(Icons.search),
),
),
),
),
),
body: Observer(
builder: (_) {
if (_store.isLoading) return const Center(child: CircularProgressIndicator());
if (_store.error != null) return Center(child: Text(_store.error!));
if (!_store.adaProduk) return const Center(child: Text('Belum ada produk'));
return ListView.builder(
itemCount: _store.produkTerfilter.length,
itemBuilder: (context, index) {
final produk = _store.produkTerfilter[index];
return ListTile(
title: Text(produk.nama),
subtitle: Text('Rp ${produk.harga}'),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _store.hapusProduk(produk.id),
),
);
},
);
},
),
);
}
}
Integrasi MobX dengan Provider #
Untuk membagikan Store ke banyak widget, gabungkan MobX dengan Provider:
// Inject Store via Provider
void main() {
runApp(
MultiProvider(
providers: [
Provider<ProdukRepository>(create: (_) => ProdukRepositoryImpl()),
// Store disediakan via Provider
ProxyProvider<ProdukRepository, ProdukStore>(
create: (context) => ProdukStore(context.read()),
update: (_, repo, store) => store!..updateRepository(repo),
),
],
child: const MyApp(),
),
);
}
// Akses di widget
class ProdukList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store = context.read<ProdukStore>();
return Observer(
builder: (_) => ListView.builder(
itemCount: store.produkTerfilter.length,
itemBuilder: (_, i) => ProdukTile(produk: store.produkTerfilter[i]),
),
);
}
}
Ringkasan #
- MobX menggunakan TFRP — koneksi antara state dan UI terjadi secara otomatis melalui mekanisme observasi, bukan secara eksplisit.
@observablemenandai state yang dipantau. GunakanObservableListdanObservableMapalih-alih collection biasa agar mutasi isi ter-track.@actionmenandai method yang mengubah state. Semua perubahan di dalam satu action di-batch — Observer hanya di-notify sekali setelah action selesai.@computedadalah state turunan yang di-memoize — hanya dihitung ulang saat Observable yang digunakan berubah. Ikuti prinsip “apa yang bisa diturunkan, harus diturunkan”.- Reaction (
autorun,reaction,when) untuk side effect otomatis — selalu simpan disposer dan panggil saat widget dihancurkan.Observerwidget otomatis rebuild ketika Observable yang digunakan di builder-nya berubah — isolasi Observer sekecil mungkin untuk performa optimal.- Jalankan
build_runnersetelah mengubah Store untuk meregenerasi file.g.dart. Gunakanwatchmode selama development.- Gabungkan MobX dengan Provider untuk dependency injection Store ke seluruh widget tree.
← Sebelumnya: Bloc & Cubit Berikutnya: Perbandingan & Kapan Memilih →