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

🧠 Test Your Knowledge

What decorator is used to mark state that can be observed?