3

States Rebuilder – ZERO Boilerplate Flutter State Management

It seems as if good state management comes with some hidden fees. Those can be in the form of boilerplate, extensive code generation or unmaintainability. ChangeNotifier is just too simple and there's a substantial amount of boilerplate when you move away from "counter apps". MobX has a lot of specifics and code gen. BLoC uses Streams which not everybody is comfortable with and there's also quite a bit of boilerplate.

On the other hand, states_rebuilder is the simplest yet most feature-packed state management package out there, and I don't say it lightly. As a bonus, there is LITERALLY no boilerplate .

The project we will build

You're going to learn states_rebuilder by building a real(ish) weather forecast app. This is a tradition here, on Reso Coder, so you can compare the app we build today with it's BLoC and MobX counterparts. Here's how it will look like:

The foundations of the project together with the basic UI are already laid down in the starter project. It contains a Weather model class holding a city name and a temperature. There's also a fake WeatherRepository which simulates fetching the forecast over a network by simply generating a random temperature.

Adding a dependency

All you need for state management with states_rebuilder comes bundled in a single package.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  states_rebuilder: ^1.10.0

Creating a WeatherStore

The official docs are kind of vague in how you should name the class holding the state management logic. That's probably because it's a pure Dart class without any dependencies on states_rebuilder whatsoever. Let's borrow the naming convention from MobX and create a WeatherStore.

Because we want to show a weather forecast, it will have a field holding the Weather model class. The UI will call getWeather() to kick off the fake network request.

weather_store.dart

class WeatherStore {
  final WeatherRepository _weatherRepository;

  WeatherStore(this._weatherRepository);

  Weather _weather;
  Weather get weather => _weather;

  void getWeather(String cityName) async {
    _weather = await _weatherRepository.fetchWeather(cityName);
  }
}

How can something this simple be useful? I mean, how is the UI going to display a CircularProgressIndicator when there's no loading field? How will the UI even know when to rebuild since there doesn't seem to be any call to an equivalent notifyListeners()?

This will all be handled by states_rebuilder's ReactiveModel wrapper class. This is absolutely amazing, because as you can see, there is not a single line of code which isn't just pure Dart. Testing this class will be a total bliss.

Injecting the store

The states_rebuilder package injects classes through the service locator pattern, much like get_it. This is on the contrary with injection libraries such as provider which utilize Flutter's widget tree for storing and looking up dependencies by using an InheritedWidget. Don't make this fool you though! The Injector class is still a StatefulWidget and the injected dependencies still get unregistered in dispose().

While you can technically inject any class with the Injector, it supports only singletons and is really geared towards holding classes which should be wrapped in a ReactiveModel at some point. Use a different DI solution for other classes.

Let's make the WeatherStore available to the WeatherSearchPage. We're going to instantiate the simplest possible Injector widget. It has many useful callbacks though, so check out the docs.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Weather App',
      home: Injector(
        inject: [
          Inject<WeatherStore>(() => WeatherStore(FakeWeatherRepository())),
        ],
        builder: (context) => WeatherSearchPage(),
      ),
    );
  }
}

Building the UI

The starter project already contains much of the WeatherSearchPage UI already set up. We just need to make it interactive by reacting to the state of the WeatherStore. That's what the StateBuilder widget is for!

weather_search_page.dart

class WeatherSearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        ...
        child: StateBuilder<WeatherStore>(
          // getAsReactive returns ReactiveModel<WeatherStore>
          models: [Injector.getAsReactive<WeatherStore>()],
          builder: (context, reactiveModel) {
            if (reactiveModel.isWaiting) {
              return buildLoading();
            } else if (reactiveModel.hasData) {
              return buildColumnWithData(reactiveModel.state.weather);
            }
            // isIdle or hasError
            return buildInitialInput();
          },
        ),
      ),
    );
  }
  ...
}
ReactiveModel is what allows the WeatherStore class not to be dependent on any external classes. It observes and notifies us about everything going on in the store. As you can see, that includes even the status any Future within the store.

The UI has to always communicate with the ReactiveModel wrapper and not with the "dumb" store directly.

The code above will work perfectly fine. However, if you don't like if statements and you'd rather use an exhaustive switch, there's also a whenConnectionState method.

weather_search_page.dart

...
builder: (context, reactiveModel) {
  return reactiveModel.whenConnectionState(
    onIdle: () => buildInitialInput(),
    onWaiting: () => buildLoading(),
    onData: (store) => buildColumnWithData(store.weather),
    onError: (_) => buildInitialInput(),
  );
},
...

Initializing state change

Whenever a new city name is submitted, the getWeather() method on the WeatherStore should be called. All the way down inside weather_search_page.dart, the starter project has a submitCityName() method. In it, let's again use the Injector to get a ReactiveModel<WeatherStore> and set the state.

weather_search_page.dart

void submitCityName(BuildContext context, String cityName) {
  final reactiveModel = Injector.getAsReactive<WeatherStore>();
  reactiveModel.setState(
    (store) => store.getWeather(cityName),
  );
}
It's again extremely important to not call methods on the "dumb" store directly. ReactiveModel is what makes state management possible.

Showing a SnackBar as a side effect

We're building widgets in reaction to state changes, we're calling getWeather() properly but there's still one thing missing for the app to be complete. We need to notify the user whan there's some error by showing a SnackBar. Showing dialogs and snack bars are examples of side effects, because rebuilding the UI is not necessary.

There are multiple ways ways to perform side effects with states_rebuilder. Since this tutorial isn't about showing you every single crevice in it's public interface, I'll show you the one most suited and straightforward for reacting to errors. That's the onError callback inside setState.

There's only one error ( NetworkError ) we're aware of that can be thrown from the WeatherRepository and we'll want to rethrow any unexpected ones. Learn why in a tutorial dedicated to error handling.

weather_search_page.dart

void submitCityName(BuildContext context, String cityName) {
  final reactiveModel = Injector.getAsReactive<WeatherStore>();
  reactiveModel.setState(
    (store) => store.getWeather(cityName),
    // Handle errors from the call site and without any setup on our side
    onError: (context, error) {
      if (error is NetworkError) {
        Scaffold.of(context).showSnackBar(
          SnackBar(
            content: Text("Couldn't fetch weather. Is the device online?"),
          ),
        );
      } else {
        // Rethrow an unexpected error
        throw error;
      }
    },
  );
}

This is all the code it takes to manage state with states_rebuilder. Boilerplate is just not present and the fact that we don't have to extend WeatherStore with some package-specific class or that we don't have to run code generation is also amazing.

As of now, this package is quite unknown, so go ahead and give it a like on pub and a star on GitHub to show its author some support.

Matt Rešetár
 

Matt is an app developer with a knack for teaching others. Working as a senior software developer at HandCash, freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

  • Mellati Fateh says:

    In your written tutorial, you suggest using other DI solutions for classes that are not supposed to be wrapped with ReactionModel because the injector only uses singletons.

    I would like to draw your attention here the following points:

    1 – Injector registers classes in the initState and unregisters them in the dispose state.
    2 – You can use nested Injectors and any class should be injected deep in the widget tree when first need.
    3 – Classes are registered lazily, they are not instantiated until first needed.
    4 – You can register with concrete types or abstract classes.

    Taking this into consideration :
    – The equivalent of registerSingleton in get_it is to inject the class at the topmost widget so it will be available to all the app. (you can set isLazy to false).

    – The equivalent of registerLazySingleton is the default behavior. Even more appropriate, you can inject the class deep in the widget tree where they first need. You benefit from the fact that when you go back in the widget tree, the Injector is disposed, and the model is unregistred because is no longer used by the app . This is contrary to what happens with get_it, if you go up in the widget tree, the model remains saved even not necessary. Imagine that a user navigates through all the screens available in the application, he will instantiate the lazy singletons which will remain alive even if the user returns to the home screen. With Injector, you can manage to destroy them and take advantage of this opportunity to dispose resources.

    – The equivalent of registerFactory is the same as above, that is, injecting the class when first needed deep in the widget tree. Whenever you go down the widget tree you instantiate the class (in intState) and if you go up you destroy it (in dispose), and so forth each time a new instance is created. As a bonus, you can get the registered instance using Injector.get method, wheres with get_it you can not get a the same instance registered with registerFactory using the GetIt.call method.

    – With Injector, you can inject futures and use whenConnectionState to display useful information to the user and finally you can get the registered instance using Injector.get method anywhere in the app. This is useful for instantiating plug-ins such as SharedPreferences. So you do not have to make the main function async and wait before calling runApp and use WidgetsFlutterBinding.ensureInitialized().
    For example, you can show a splash screen informing the user that something is instantiating and display a helping error message if a plug-in fails to initialize.

  • Ernesto Cuesy says:

    Hi Matej –

    Thanks for the tutorial, it is very good. Question: do you recommend mixing this state management framework with the other known frameworks like Bloc, Provider, etc in the same app or should one choose one and stick to it? I suppose that in order to be consistent and have more maintainable code one should just choose one, right? But do you think states rebuilder covers all the use cases that other frameworks do? Thanks!

  • anan alfred says:

    void submitCityName(BuildContext context, String cityName) {
    final reactiveModel = Injector.getAsReactive();
    reactiveModel.setState(
    (store) => store.getWeather(cityName),
    );
    }

    how can i call this on initstate ?

  • >