MobX State Management
Reactive state management for Flutter
🔄 What is MobX?
MobX is a reactive state management library that automatically tracks dependencies and updates UI when state changes. It uses observables, actions, and reactions to create predictable, scalable apps with minimal boilerplate code and maximum developer productivity.
// Observable state automatically updates UI
@observable
int counter = 0;
MobX Core Concepts
Observables
State that can be observed
@observable
String name = 'John';
Actions
Methods that modify state
@action
void increment() {
counter++;
}
Computed
Derived values from state
@computed
String get fullName =>
'$firstName $lastName';
Reactions
Side effects when state changes
reaction(
(_) => counter,
(value) => print(value),
)
🔹 Installation
Add MobX packages to your project:
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
mobx: ^2.3.0
flutter_mobx: ^2.2.0
dev_dependencies:
build_runner: ^2.4.0
mobx_codegen: ^2.6.0
🔹 Basic Counter Store
Create a simple MobX store:
import 'package:mobx/mobx.dart';
// Include generated file
part 'counter_store.g.dart';
// This is the class used by rest of your codebase
class CounterStore = _CounterStore with _$CounterStore;
// The store-class
abstract class _CounterStore with Store {
@observable
int counter = 0;
@action
void increment() {
counter++;
}
@action
void decrement() {
counter--;
}
@action
void reset() {
counter = 0;
}
}
// Run: flutter pub run build_runner build
🔹 Using Store in Widget
Connect MobX store to Flutter widgets:
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class CounterPage extends StatelessWidget {
final CounterStore store = CounterStore();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MobX Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Counter Value:',
style: TextStyle(fontSize: 20),
),
// Observer automatically rebuilds when counter changes
Observer(
builder: (_) => Text(
'${store.counter}',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: store.decrement,
child: Icon(Icons.remove),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: store.reset,
child: Text('Reset'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: store.increment,
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}
🔹 Computed Values
Derive values automatically from observables:
import 'package:mobx/mobx.dart';
part 'user_store.g.dart';
class UserStore = _UserStore with _$UserStore;
abstract class _UserStore with Store {
@observable
String firstName = '';
@observable
String lastName = '';
@observable
int age = 0;
// Computed values automatically update
@computed
String get fullName => '$firstName $lastName';
@computed
bool get isAdult => age >= 18;
@computed
String get greeting => 'Hello, $fullName!';
@action
void updateName(String first, String last) {
firstName = first;
lastName = last;
}
@action
void updateAge(int newAge) {
age = newAge;
}
}
// Usage in widget
Observer(
builder: (_) => Column(
children: [
Text('Full Name: ${userStore.fullName}'),
Text('Is Adult: ${userStore.isAdult}'),
Text(userStore.greeting),
],
),
)
🔹 Observable Collections
Work with lists, sets, and maps:
import 'package:mobx/mobx.dart';
part 'todo_store.g.dart';
class TodoStore = _TodoStore with _$TodoStore;
abstract class _TodoStore with Store {
@observable
ObservableList todos = ObservableList();
@computed
int get todoCount => todos.length;
@computed
bool get hasTodos => todos.isNotEmpty;
@action
void addTodo(String todo) {
todos.add(todo);
}
@action
void removeTodo(int index) {
todos.removeAt(index);
}
@action
void clearAll() {
todos.clear();
}
}
// Usage
class TodoList extends StatelessWidget {
final TodoStore store = TodoStore();
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (_) => Text('Total: ${store.todoCount}'),
),
Expanded(
child: Observer(
builder: (_) => ListView.builder(
itemCount: store.todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(store.todos[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => store.removeTodo(index),
),
);
},
),
),
),
],
);
}
}
🔹 Reactions
React to state changes with side effects:
import 'package:mobx/mobx.dart';
part 'cart_store.g.dart';
class CartStore = _CartStore with _$CartStore;
abstract class _CartStore with Store {
@observable
int itemCount = 0;
@observable
double totalPrice = 0.0;
late List _disposers;
void setupReactions() {
_disposers = [
// Reaction: runs when observable changes
reaction(
(_) => itemCount,
(count) {
print('Item count changed to: $count');
},
),
// AutoRun: runs immediately and on changes
autorun((_) {
print('Total price: \$${totalPrice}');
}),
// When: runs once when condition becomes true
when(
(_) => totalPrice > 100,
() {
print('Free shipping unlocked!');
},
),
];
}
void dispose() {
// Clean up reactions
for (final disposer in _disposers) {
disposer();
}
}
@action
void addItem(double price) {
itemCount++;
totalPrice += price;
}
@action
void removeItem(double price) {
if (itemCount > 0) {
itemCount--;
totalPrice -= price;
}
}
}
🔹 Async Actions
Handle asynchronous operations:
import 'package:mobx/mobx.dart';
part 'data_store.g.dart';
class DataStore = _DataStore with _$DataStore;
abstract class _DataStore with Store {
@observable
bool isLoading = false;
@observable
String? error;
@observable
List data = [];
@action
Future fetchData() async {
isLoading = true;
error = null;
try {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
// Update state
runInAction(() {
data = ['Item 1', 'Item 2', 'Item 3'];
isLoading = false;
});
} catch (e) {
runInAction(() {
error = e.toString();
isLoading = false;
});
}
}
@action
void clearData() {
data.clear();
error = null;
}
}
// Usage
class DataWidget extends StatelessWidget {
final DataStore store = DataStore();
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
if (store.isLoading) {
return CircularProgressIndicator();
}
if (store.error != null) {
return Text('Error: ${store.error}');
}
return ListView.builder(
itemCount: store.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(store.data[index]),
);
},
);
},
);
}
}
🔹 Form Validation with MobX
Manage form state reactively:
import 'package:mobx/mobx.dart';
part 'login_store.g.dart';
class LoginStore = _LoginStore with _$LoginStore;
abstract class _LoginStore with Store {
@observable
String email = '';
@observable
String password = '';
@computed
bool get isEmailValid => email.contains('@') && email.length > 5;
@computed
bool get isPasswordValid => password.length >= 6;
@computed
bool get canLogin => isEmailValid && isPasswordValid;
@computed
String? get emailError =>
email.isEmpty ? null : (isEmailValid ? null : 'Invalid email');
@computed
String? get passwordError =>
password.isEmpty ? null : (isPasswordValid ? null : 'Min 6 characters');
@action
void setEmail(String value) {
email = value;
}
@action
void setPassword(String value) {
password = value;
}
@action
Future login() async {
if (canLogin) {
print('Logging in with $email');
// Perform login
}
}
}
// Usage
class LoginForm extends StatelessWidget {
final LoginStore store = LoginStore();
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (_) => TextField(
onChanged: store.setEmail,
decoration: InputDecoration(
labelText: 'Email',
errorText: store.emailError,
),
),
),
Observer(
builder: (_) => TextField(
onChanged: store.setPassword,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: store.passwordError,
),
),
),
Observer(
builder: (_) => ElevatedButton(
onPressed: store.canLogin ? store.login : null,
child: Text('Login'),
),
),
],
);
}
}
🔹 Code Generation
Generate MobX boilerplate code:
# One-time build
flutter pub run build_runner build
# Watch for changes (recommended during development)
flutter pub run build_runner watch
# Delete conflicting outputs
flutter pub run build_runner build --delete-conflicting-outputs
Code Generation Tips:
- Watch mode: Automatically rebuilds on file changes
- Part directive: Always include part 'filename.g.dart'
- Clean build: Use --delete-conflicting-outputs if errors occur
- Git ignore: Add *.g.dart to .gitignore (optional)
💡 MobX Best Practices:
- Single Store: One store per feature or domain
- Actions Only: Modify observables only in @action methods
- Computed Values: Use @computed for derived state
- Observer Widgets: Wrap only parts that need updates
- Dispose Reactions: Clean up reactions to prevent memory leaks
- RunInAction: Use for async state updates