MobX is a scalable library developed to ease the process of state management in frontend applications. In the course of this article, you will learn how to manage the state of your Flutter applications using MobX, and at the end of the article, you’ll setup a continuous integration/continuous deployment (CI/CD) pipeline for your application using CircleCI. You can find the sample project developed throughout this article in this GitHub repository.

Prerequisites

Before getting started with this article, you need a working knowledge of Flutter. If you need help getting started, you can follow the codelabs on the Flutter website. You also need to have the installations outlined below on your machine:

However, irrespective of the choice of IDE used, to aid effective development through the provision of tools for editing and refactoring your Flutter application, you will need an installation of the Dart and Flutter plugins.

Overview

History of MobX

According to mobx.js.org, MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP). Developed primarily with React applications in mind, MobX has grown to support applications built with other JavaScript libraries, and in recent times, Flutter applications.

Working principles

Using MobX for managing state management in Flutter apps utilizes three of the library’s main concepts - the observable state, actions, and computed values.

Observable states are properties of an application that are susceptible to change and are declared with the @observable annotation. For example, observable states in a to-do application includes primarily the list of all to-dos and every other property whose value may be updated.

Actions are operations that are aimed at changing the value of an observable state and they are usually declared with the @action annotation. Upon running an action, MobX handles updating parts of the application that utilize observables modified by the action. An example of actions is a function to update the list of to-dos with a new to-do in a to-do application.

Lastly, computed values are similar to observable states and are declared with the @computed annotation. However, as opposed to their values depending directly on actions, they depend on the value of observable states and as such, if the observable state that a computed value depends on is modified by an action, the computed value is also correspondingly updated. In practice, developers often overlook the idea of computed values and instead, often unintentionally, use observables in their place.

Comparing paradigms - MobX, BLoC, Redux, and setState()

Unlike other state management patterns in Flutter such as BLoC, which was built on the principle of using streams to propagate changes, and Redux, which was built on the philosophy that an application possesses a single source of truth from which it’s widgets inherit, MobX was built on the simple philosophy that anything that can be derived from the application state, should be derived. This implies that MobX provides coverage for all properties in an application state that are defined with the likelihood to change and rebuilds the UI only when such properties change. MobX can be viewed as providing a similar level of simplicity as setState(), except that it relieves you of having to handle state propagation all by yourself. Its ability to abstract the state-changing details affords the library a smoother learning curve relative to other approaches.

Project setup

To facilitate the process of creating a new Flutter project, you are going to use the Flutter CLI tool. To do this, open your terminal and navigate to your project’s directory to run the following command:

$ flutter create reviewapp

The CLI tool generates a template project to get you started within a couple of seconds. After project generation, you can open the project in your IDE.

Installing dependencies

As you will see in the course of the article, the project requires five main dependencies:

  • mobx: Dart port of MobX for writing state-modifying logic
  • flutter_mobx: Flutter integration for MobX that provides the Observer widget that automatically rebuilds based on changes to observable state
  • shared_preferences: a local persistence library
  • mobx_codegen: a code generation library for MobX that allows usage of MobX annotations
  • build_runner: a stand-alone library to run code generation operations

With the project open in your IDE, navigate to your /pubspec.yaml file to add the dependencies by replacing the dependencies section with the snippet below:

dependencies:
  flutter:
    sdk: flutter
  mobx: ^0.3.5
  flutter_mobx: ^0.3.0+1
  shared_preferences: ^0.5.3+4

Then, in a similar fashion, replace the dev_dependencies section with the code snippet below:

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.6.5
  mobx_codegen: ^0.3.3+1

Finally, run the following command in your project’s root directory to download the dependencies:

$ flutter packages get

What you’ll build

In the course of this article, in a bid to establish your knowledge of the fundamentals of MobX as a Flutter state management pattern, you’ll build a simple review application that allows users to add comments and stars as seen in the images below:

Project structure

The sample project described in the What you’ll build section works as follows:

  • Start the app
  • Get reviews from local preferences
  • Update UI with retrieved reviews
  • Add review
  • Update list of reviews within app state
  • Persist updated list of reviews in preferences

Finally before getting started, create the /widgets, /screens, and /models folder by running the command below in your project’s /lib directory:

$ mkdir widgets screens models

Creating data models

To start with, define a data model for the reviews by creating a reviewmodel.dart file in the /lib/models/ directory and adding the code snippet below to it:

import 'package:meta/meta.dart';
class ReviewModel {
  final String comment;
  final int stars;
  const ReviewModel({@required this.comment, @required this.stars});

  factory ReviewModel.fromJson(Map<String, dynamic> parsedJson) {
    return ReviewModel(
      comment: parsedJson['comment'],
      stars: parsedJson['stars'],
    );
  }

  Map<String, dynamic> toJson(){
    return {
      'comment': this.comment,
      'stars': this.stars,
    };
  }
}

Creating the user interface

As seen in the What you’ll build section, the application contains a review screen that allows users to add reviews and at the same time display a list of existing reviews, the total number of reviews and the average stars obtained from the reviews.

To create the Review screen, create a review.dart file in the /lib/screens directory and add the code snippet below:

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../widgets/info_card.dart';

class Review extends StatefulWidget {
  @override
  ReviewState createState() {
    return new ReviewState();
  }
}
class ReviewState extends State<Review> {
  final List<int> _stars = [1, 2, 3, 4, 5];
  final TextEditingController _commentController = TextEditingController();
  int _selectedStar;

  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Scaffold(
      appBar: AppBar(
        title: Text('Review App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 12.0),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: <Widget>[
                Container(
                  width: screenWidth * 0.6,
                  child: TextField(
                    controller: _commentController,
                    decoration: InputDecoration(
                      contentPadding: EdgeInsets.all(10),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(10.0),
                      ),
                      hintText: "Write a review",
                      labelText: "Write a review",
                    ),
                  ),
                ),
                Container(
                  child: DropdownButton(
                    hint: Text("Stars"),
                    elevation: 0,
                    value: _selectedStar,
                    items: _stars.map((star) {
                      return DropdownMenuItem<int>(
                        child: Text(star.toString()),
                        value: star,
                      );
                    }).toList(),
                    onChanged: (item) {
                      setState(() {
                        _selectedStar = item;
                      });
                    },
                  ),
                ),
                Container(
                  child: Builder(
                    builder: (BuildContext context) {
                      return IconButton(
                        icon: Icon(Icons.done),
                        onPressed: () {},
                      );
                    },
                  ),
                ),
              ],
            ),
            SizedBox(height: 12.0),
            //contains average stars and total reviews card
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                InfoCard(
                    infoValue: '2',
                    infoLabel: "reviews",
                    cardColor: Colors.green,
                    iconData: Icons.comment),
                InfoCard(
                  infoValue: '2',
                  infoLabel: "average stars",
                  cardColor: Colors.lightBlue,
                  iconData: Icons.star,
                  key: Key('avgStar'),
                ),
              ],
            ),
            SizedBox(height: 24.0),
            //the review menu label
            Container(
              color: Colors.grey[200],
              padding: EdgeInsets.all(10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Icon(Icons.comment),
                  SizedBox(width: 10.0),
                  Text(
                    "Reviews",
                    style: TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
            //contains list of reviews
            Expanded(
              child: Container(
                child: Text("No reviews yet"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Creating custom widgets

In the code snippet above, a reference is made to InfoCard, a custom widget to display the total number of reviews and average stars as seen below:

To create the InfoCard widget, create a file info_card.dart in the /lib/widgets directory and add the code snippet below:

import 'package:flutter/material.dart';

class InfoCard extends StatelessWidget {
  final String infoValue;
  final String infoLabel;
  final Color cardColor;
  final IconData iconData;
  const InfoCard(
      {Key key,
      @required this.infoValue,
      @required this.infoLabel,
      @required this.cardColor,
      @required this.iconData,
      })
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Container(
      height: 100,
      width: screenWidth / 2,
      child: Card(
        color: cardColor,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(5.0),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            Icon(
              iconData,
              size: 28.0,
              color: Colors.white,
            ),
            Text(
              "$infoValue $infoLabel",
              style: TextStyle(color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }
}

Lastly, albeit not referred to until the latter stages of this article, create a ReviewWidget class which will be used to display a single review item. To do this, create a review.dart file in the project’s lib/widgets directory and add the code snippet below:

import 'package:flutter/material.dart';
import '../models/reviewmodel.dart';
import '../models/reviews.dart';
import '../widgets/review.dart';
import '../widgets/info_card.dart';

class ReviewWidget extends StatelessWidget {
  final ReviewModel reviewItem;

  const ReviewWidget({Key key, @required this.reviewItem}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Padding(
          padding: EdgeInsets.all(10.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Expanded(
                child: Text(
                  reviewItem.comment,
                ),
              ),
              Row(
                children: List(reviewItem.stars).map((listItem) {
                  return Icon(Icons.star);
                }).toList(),
              ),
            ],
          ),
        ),
        Divider(
          color: Colors.grey,
        )
      ],
    );
  }
}

Implementing MobX

Implementing MobX in your application involves defining necessary observables, actions and computed values (as highlighted in the Working principles section above) in our application state.

In the review application, at any point in time, the list of reviews, average number of stars and total number of reviews must be the most-recent value available. This implies that they must be declared with necessary annotations such that changes to them are tracked by MobX.

To do this, create a file reviews.dart in the /lib/models directory of your project and add the code snippet below:

import 'dart:async';
import 'dart:convert';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import './reviewmodel.dart';
part 'reviews.g.dart';
class Reviews = ReviewsBase with _$Reviews;
abstract class ReviewsBase with Store {
  @observable
  ObservableList<ReviewModel> reviews = ObservableList.of([]);

  @observable
  double averageStars = 0;

  @computed
  int get numberOfReviews => reviews.length;

  int totalStars = 0;

  @action
  void addReview(ReviewModel newReview) {
    //to update list of reviews
    reviews.add(newReview);
    // to update the average number of stars
    averageStars = _calculateAverageStars(newReview.stars);
    // to update the total number of stars
    totalStars += newReview.stars;
    // to store the reviews using Shared Preferences
    _persistReview(reviews);
  }

  @action
  Future<void> initReviews() async {
    await _getReviews().then((onValue) {
      reviews = ObservableList.of(onValue);
      for (ReviewModel review in reviews) {
        totalStars += review.stars;
      }
    });
    averageStars = totalStars / reviews.length;
  }

  double _calculateAverageStars(int newStars) {
    return (newStars + totalStars) / numberOfReviews;
  }

  void _persistReview(List<ReviewModel> updatedReviews) async {
    List<String> reviewsStringList = [];
    SharedPreferences _preferences = await SharedPreferences.getInstance();
    for (ReviewModel review in updatedReviews) {
      Map<String, dynamic> reviewMap = review.toJson();
      String reviewString = jsonEncode(ReviewModel.fromJson(reviewMap));
      reviewsStringList.add(reviewString);
    }
    _preferences.setStringList('userReviews', reviewsStringList);
  }
  
  Future<List<ReviewModel>> _getReviews() async {
    final SharedPreferences _preferences =
        await SharedPreferences.getInstance();
    final List<String> reviewsStringList =
        _preferences.getStringList('userReviews') ?? [];
    final List<ReviewModel> retrievedReviews = [];
    for (String reviewString in reviewsStringList) {
      Map<String, dynamic> reviewMap = jsonDecode(reviewString);
      ReviewModel review = ReviewModel.fromJson(reviewMap);
      retrievedReviews.add(review);
    }
    return retrievedReviews;
  }
}

The code snippet above declares two variables: reviews, which is a list containing all user reviews at any point in time, and averageStars, which is the average number of stars computed from all reviews as observables because their values are expected to change in response to an action. It then defines the addReview() function which adds a new review to the list of reviews, and an initReviews() function which initializes the list of reviews with existing data from shared preferences as actions aimed at updating the observable states.

Additionally, although the numberOfReviews variable can also be declared as an observable, a computed value is used instead, solely because changes to its value are dependent on the result of an action (updated observable state) rather than directly on the action itself. Think of it as an aftermath effect. Lastly, a totalStars variable and the functions _calculateAverageStars(), _persistReview(), and _getReviews() are declared, but in this case, with no annotations as they’re helper parameters that do not directly update state.

Running CodeGen

As a result of MobX’s focus on abstracting high-level implementation details, the library handles the process of generating a data store as opposed to the case in Redux where even stores are manually written. MobX performs code generation by leveraging on its mobx_codegen library alongside Dart’s build_runner library, and takes all annotated properties into consideration when scaffolding a store.

To generate the store, run the command below in your project’s root directory, after which you’ll find a review.g.dart file in your /lib/models directory:

$ flutter packages pub run build_runner build

Using observers

Even with a MobX store implemented, reflecting state changes in your application’s UI requires the use of Observers from the flutter_mobx library. Precisely, an observer is a widget that wraps around an observable or a computed value to render changes in their values to the UI.

As seen in the What you’ll build section, the values of average stars, number of reviews, and all reviews get updated as a new review is added, implying that the widgets used in rendering the said values require being wrapped in an Observer widget. To use the observer widget, navigate to your /lib/screens/review.dart file and modify the ReviewState class, leaving it as seen in the code below:

class ReviewState extends State<Review> {
  final Reviews _reviewsStore = Reviews();
  final TextEditingController _commentController = TextEditingController();  
  final List<int> _stars = [1, 2, 3, 4, 5];
  int _selectedStar;
  @override
  void initState() {
    _selectedStar = null;
    _reviewsStore.initReviews();
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Scaffold(
      appBar: AppBar(
        title: Text('Review App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 12.0),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: <Widget>[
                Container(
                  width: screenWidth * 0.6,
                  child: TextField(
                    controller: _commentController,
                    decoration: InputDecoration(
                      contentPadding: EdgeInsets.all(10),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(10.0),
                      ),
                      hintText: "Write a review",
                      labelText: "Write a review",
                    ),
                  ),
                ),
                Container(
                  child: DropdownButton(
                    hint: Text("Stars"),
                    elevation: 0,
                    value: _selectedStar,
                    items: _stars.map((star) {
                      return DropdownMenuItem<int>(
                        child: Text(star.toString()),
                        value: star,
                      );
                    }).toList(),
                    onChanged: (item) {
                      setState(() {
                        _selectedStar = item;
                      });
                    },
                  ),
                ),
                Container(
                  child: Builder(
                    builder: (BuildContext context) {
                      return IconButton(
                        icon: Icon(Icons.done),
                        onPressed: () {
                          if (_selectedStar == null) {
                            Scaffold.of(context).showSnackBar(SnackBar(
                              content:
                                  Text("You can't add a review without star"),
                              duration: Duration(milliseconds: 500),
                            ));
                          } else if (_commentController.text.isEmpty) {
                            Scaffold.of(context).showSnackBar(SnackBar(
                              content: Text("Review comment cannot be empty"),
                              duration: Duration(milliseconds: 500),
                            ));
                          } else {
                            _reviewsStore.addReview(ReviewModel(
                                comment: _commentController.text,
                                stars: _selectedStar));
                          }
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
            SizedBox(height: 12.0),
            //contains average stars and total reviews card
            Observer(
              builder: (_) {
                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    InfoCard(
                      infoValue: _reviewsStore.numberOfReviews.toString(),
                      infoLabel: "reviews",
                      cardColor: Colors.green,
                      iconData: Icons.comment
                    ),
                    InfoCard(
                      infoValue: _reviewsStore.averageStars.toStringAsFixed(2),
                      infoLabel: "average stars",
                      cardColor: Colors.lightBlue,
                      iconData: Icons.star,
                      key: Key('avgStar'),
                    ),
                  ],
                );
              },
            ),
            SizedBox(height: 24.0),
            //the review menu label
            Container(
              color: Colors.grey[200],
              padding: EdgeInsets.all(10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Icon(Icons.comment),
                  SizedBox(width: 10.0),
                  Text(
                    "Reviews",
                    style: TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
            //contains list of reviews
            Expanded(
              child: Container(
                child: Observer(
                  builder: (_) => _reviewsStore.reviews.isNotEmpty
                      ? ListView(
                          children:
                              _reviewsStore.reviews.reversed.map((reviewItem) {
                            return ReviewWidget(
                              reviewItem: reviewItem,
                            );
                          }).toList(),
                        )
                      : Text("No reviews yet"),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

The code snippet above appends the first modification by creating an instance of the Review class from /lib/models/reviews.dart as a means of accessing the store. It then wraps the Row within which the average stars and total reviews data are displayed with an observer widget before using the reviewStore instance of the Review class to refer to the aforementioned data.

Then, the placeholder “no reviews” Text widget is made to display when the list of reviews in the store is empty, otherwise a ListView displaying the items of the list. Finally, the onPressed() function of the “Done” button is modified to add a new review to the store.

At this point, your application is almost complete. To see the application in action, first import the review screen by adding the snippet below to the imports section of your main.dart file:

$ import './screens/review.dart';

Then, modify the home attribute in the build() method of the MyApp class in /lib/main.dart from MyHomePage() to Review(), leaving the method as seen in the code below:

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Review() //previously MyHomePage(),
  );
}

Then, run the app using the flutter run command.

Writing sample tests

For the purpose of understanding how tests fit into a CI/CD pipeline in the CI/CD with CircleCI section bleow, you’ll need to create a simple unit test and widget test.

To write the unit test, create a file called unit_test.dart in the project’s /test directory and add the code snippet below:

import 'package:flutter_test/flutter_test.dart';
import '../lib/models/reviewmodel.dart';
import '../lib/models/reviews.dart';

void main() {
  test('Test MobX state class', () async {
    final Reviews _reviewsStore = Reviews();

    _reviewsStore.initReviews();

    expect(_reviewsStore.totalStars, 0);

    expect(_reviewsStore.averageStars, 0);
    _reviewsStore.addReview(ReviewModel(
      comment: 'This is a test review',
      stars: 3,
    ));

    expect(_reviewsStore.totalStars, 3);
    _reviewsStore.addReview(ReviewModel(
      comment: 'This is a second test review',
      stars: 5,
    ));

    expect(_reviewsStore.averageStars, 4);
  });
}

Then, add the widget test by totally replacing the contents of the existing widget_test.dart file in your project’s test directory with the code snippet below:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';

void main() {
  testWidgets('Test for rendered UI', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    Finder starCardFinder = find.byKey(Key('avgStar'));

    expect(starCardFinder, findsOneWidget);
  });
}

You can run the tests by executing the flutter test command in your project’s root directory.

CI/CD with CircleCI

Beyond just providing an environment for you to build your projects, continuous integration (CI) tools provide a reliable and stable environment to run automated tests and they can auto-upload deploy artifacts. In this section, you will learn how to setup and utilize a CI/CD pipeline for your Flutter project using CircleCI.

Git-ing the project

In setting up deployment pipelines, CI tools typically use a version control system (VCS), such as Git, to pull your project files. It monitors your project’s repository and, on a successful commit, it pulls the changes and performs predefined automated tasks on the project.

CircleCI is an easy-to-use platform that provides robust CI services for diverse types of software projects and supports Git repositories hosted online on GitHub and BitBucket. For the purpose of this article, you’ll host your repository on GitHub.

To start, initialize a local Git repository in your project by executing the git init command in your project’s root directory. Add your files to it with git add .. Commit those files with git commit -m "First commit". Then, create an online repository for your project on GitHub and add the GitHub repository as a remote reference to your local repository. Upon doing that, push changes to the remote repository by running the command below in your project’s root directory:

$ git remote add origin https://link_to_repo && git push -u origin master

Creating a configuration file

Create a folder named .circleci by running the mkdir .circleci command in your project’s root directory and create a configuration file such that the file path is given as /your_project_path/.circleci/config.yml.

Then, populate the /.circleci/config.yml file with the code snippet below:

version: 2
jobs:
  build:    
    docker:
      - image: cirrusci/flutter:v1.5.8 

    branches:
      only: master

    steps:
      - checkout

      - run:
          name: Run Flutter doctor
          command: flutter doctor

      - run:
          name: Run the application tests
          command: flutter test

      - run:
          name: Build the Android version
          command: flutter build apk 

      - store_artifacts:
          path: build/app/outputs/apk/release/app-release.apk

In the configuration file above, Docker was used as the executor. Although there is no official CircleCI Docker image for Flutter, in reality, there is a large list of Flutter images available on DockerHub with the most prominent of them being the cirrusci/flutter image with the highest usage frequency of over 100k pulls compared to 10k pulls of the next most-used image on the list.

The optional branches section is used to filter what branches to run the deployment process on. When not explicitly defined, CircleCI assumes master as the branch to work on.

In addition to defining the Docker image to be used and pinning a version of the image that matches the version of Flutter used to run the local copy of your project (v1.5.8 in my case), the snippet above also defines, in the steps section, each process to be performed every time the deployment is ran on your project’s repository in their execution order.

Lastly, in the store-artifacts section of the code snippet above, a path to our build artifact is referenced to enable automatic upload of the artifact to the Artifacts tab of your CircleCI dashboard. This artifact could be deployed to an AWS S3 bucket or any other hosting service. For production-ready Flutter applications, one could add deployment to an app store to this config.

Setting up CircleCI

Go to https://circleci.com/signup/ and sign up with your GitHub account. This ensures that CircleCI is allowed access to your project repository. To integrate CircleCI with your project, on the CircelCI dashboard, click Add Projects. It is on the far left of the dashboard’s page. Next, navigate to the far right of the page and click Set Up Project.

Add Projects Screen

A page like the one shown below will appear. Click Start Building.

Project setup interface

And your build will begin.

Building project

Now, after every commit of new code, you will have an automated build, test, and deployment pipeline for your Flutter application.

Conclusion

In this post, we covered how to manage the state of a Flutter application with the MobX state management library. We also covered how to setup a CI/CD pipeline for your application using CircleCI.

Although there are trade-offs to consider in deciding on a choice of state management approaches for a Flutter project, especially large ones, MobX offers a solid option for medium-sized projects. The library’s ability to do the hardest work for you, while giving you the power to take charge where necessary, is one of the pros to consider when making the decision for your next Flutter project, or maybe in rewriting the current one (chuckles).

Although still in its early stages, MobX is gaining considerable adoption for Flutter projects. Check out the library’s GitHub repository for different ways to contribute to its growth.

I do hope that you enjoyed this tutorial. Happy hacking!


Fabusuyi is a Software Engineering Intern at Busha where he works with the mobile development team. In addition, he’s currently a Computer Science student with a research interest in Computational Theory.