MobX for Dart

repository·main·Indexed 25 days ago

https://github.com/mobxjs/mobx.dart

A state-management library for Dart and Flutter that simplifies the connection between reactive application data and the UI through automatic dependency tracking. It features the Observer widget for reactive Flutter widgets, mobx_codegen for reducing boilerplate via @observable, @action, and @computed annotations, and mobx_lint for custom linting and refactoring assists.

Tokens
38K
Snippets
100
Records
169
Agent score
76%

What's inside mobx.dart

  1. Introduction to mobx.dart

    main

    MobX is a state-management library for Dart that uses Transparent Functional Reactive Programming (TFRP) to automatically connect reactive application data with the UI.

    Instead of manually syncing state and UI, you define observables (the data) and reactions (the side effects, such as UI re-renders or network calls). MobX automatically tracks which observables are being consumed by which reactions and re-runs the reactions whenever the underlying observables change.

  2. Explore MobX examples

    main

    The mobx_examples repository contains several practical implementations of MobX to demonstrate different usage patterns, ranging from simple state management to complex asynchronous data flows. You can explore the following examples in their respective directories:

    • Clock: Basic time-based updates.
    • Counter: Simple integer state management.
    • Signup form: Handling form state and validation.
    • Github repo search: Managing asynchronous API requests and search results.
    • Hackernews: Handling complex data structures and real-time updates.
    • Multi Counter: Managing multiple independent state instances.
    • Random Stream: Working with continuous data streams.
    • Todos: Classic CRUD (Create, Read, Update, Delete) application state.
    • Dice: Simple randomized state updates.
  3. What is an Atom and when to use it

    main

    An Atom is the fundamental building block of the MobX reactivity system. It tracks whether it is being observed and notifies the system when it changes.

    Important distinctions:

    • No Storage: An Atom does not store a value; it only manages the notification logic. Observable extends Atom to add value storage.
    • Usage: You will rarely use Atom directly in application code. Most use cases are handled by Observable.
    • Internal Use: The mobx_codegen package uses Atom internally for all @observable annotated fields.
  4. Use Actions to mutate observables

    main

    Actions are the mechanism used to mutate observables. Instead of mutating data directly, you should use actions to:

    1. Add semantic meaning to mutations (e.g., increment() instead of value++).
    2. Batch notifications so that observers are only notified once the action completes atomically.

    Actions can be nested; in such cases, notifications are only sent when the top-most action completes. You can define actions manually using the Action class or use the @action annotation with mobx_codegen.

    final counter = Observable(0);
    
    final increment = Action(() {
      counter.value++;
    });
  5. What are the core concepts of MobX?

    main

    MobX is built on a triad of three fundamental concepts that work together to create a reactive system:

    1. Observables: Represent the reactive state of your application (the data).
    2. Actions: The only way to mutate observables, providing semantic meaning to changes and batching notifications.
    3. Reactions: Observers that automatically track which observables are used and respond when those observables change.

    This cycle allows you to define state, mutate it through meaningful actions, and automatically update the UI or other parts of the system via reactions.

  6. Use Computed Observables for derived state

    main

    State in MobX is composed of Core-State (inherent data) and Derived-State (data calculated from core state).

    Computed Observables are a type of derived state that is automatically kept in sync when its underlying observables change. They are efficient because they only re-calculate when their dependencies actually change. Use the @computed annotation to define them in a class using mobx_codegen.

    import 'package:mobx/mobx.dart';
    
    part 'contact.g.dart';
    
    class Contact = ContactBase with _$Contact;
    
    abstract class ContactBase with Store {
      @observable
      String firstName;
    
      @observable
      String lastName;
    
      @computed
      String get fullName => '$firstName, $lastName';
    }
  7. Compare MobX and InheritedModel for state management

    main

    When choosing a state management solution in Flutter, consider the complexity of your application and your preference for automation versus manual control.

    MobX

    MobX is a reactive programming library that offers:

    • Reactivity: State changes are automatically reflected in the UI, reducing boilerplate.
    • Centralized State: Allows managing app state in a single place, improving debuggability.
    • High Performance: Highly optimized for complex applications with significant state.

    InheritedModel

    InheritedModel is a built-in Flutter widget that is simpler but has different characteristics:

    • Manual Updates: You must manually trigger UI updates (e.g., via notifyListeners()).
    • Decentralized State: State tends to be scattered throughout the widget tree, which can make reasoning about the app more difficult.
    • Lower Complexity: Easier to learn for simple applications where performance is not a primary concern.
  8. MobX cross-cutting concerns

    main

    Beyond the core actors, MobX provides several cross-cutting layers to support robust application development:

    • Memoization: Uses DependencyState to memoize computations.
    • Spying and Traceability: Tools for observing and tracing how state changes propagate through the system.
    • Exception Handling: Mechanisms for catching and propagating exceptions within the reactive graph.
  9. Use Observables to represent reactive state

    main

    Observables represent the state of your application. They can be simple scalars or complex object trees.

    Important Note on Deep Observability: Unlike the JavaScript version of MobX, the Dart version does not support deep observability due to the lack of reflection (dart:mirrors) in Flutter. If you have a complex object marked as @observable, MobX cannot automatically track changes to its internal fields. To track changes at the field level, you must mark each individual field with the @observable annotation.

    You can create simple observables using the Observable class, or use the mobx_codegen package to automate observable creation in classes using the @observable annotation.

    import 'package:mobx/mobx.dart';
    
    // Simple scalar observable
    final counter = Observable(0);
  10. How MobX Context works

    main

    The MobX reactive system operates within a ReactiveContext. This context tracks the relationship between observables and their linked reactions. When an observable changes, the context ensures that any linked reactions are automatically executed.

    By default, all reactivity occurs within a singleton, top-level context called mainContext.

    While most users should rely on mainContext, you can create a custom ReactiveContext to isolate reactive systems. This is particularly useful when building libraries that use MobX internally, as it prevents the library's reactivity from interfering with the host application's reactivity (e.g., a Flutter app).

  11. How to use @computed to simplify UI logic

    main

    @computed properties are powerful tools for simplifying Widget code. Instead of performing conditional checks or complex calculations inside an Observer widget, move that logic into a @computed property in your store. Because @computed properties are themselves observables, the Observer will automatically re-render when the computed value changes.

    Example: Simplifying loading state checks

    Instead of this in your Widget:

    if (store.loadOperation != null && store.loadOperation.status == FutureStatus.fulfilled) {
      return ContactView(store);
    }

    Use this in your Store:

    @observable ObservableFuture<void> loadOperation = null;
    
    @computed
    bool get hasResults => loadOperation != null && loadOperation.status == FutureStatus.fulfilled;

    And this in your Widget:

    if (store.hasResults) {
      return ContactView(store);
    }
  12. Manage asynchronous state with ObservableFuture

    main

    Instead of managing raw Future objects and separate loading/error/data variables, you can use ObservableFuture to represent the entire lifecycle of an asynchronous operation. An ObservableFuture tracks three distinct states via its status property:

    1. FutureStatus.pending: The operation is in progress (e.g., showing a loading indicator).
    2. FutureStatus.fulfilled: The operation completed successfully; the data is available in the result field.
    3. FutureStatus.rejected: The operation failed; error details are available.

    In a MobX store, you can update an @observable ObservableFuture by assigning a new instance of ObservableFuture(yourFuture) inside an @action.

    @observable
    ObservableFuture<List<FeedItem>>? latestItemsFuture;
    
    @action
    Future fetchLatest() => latestItemsFuture = ObservableFuture(_hnApi.newest());