• Flutter State Management: setState, BLoC, ValueNotifier, Provider
  • Originally by Andrea Bizzotto
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: talisk
  • Proofreader: Fxy4ever

Flutter state management schemes include setState, BLoC, ValueNotifier, and Provider

In this article, the focus of this video, we compare different state management schemes.

For example, we use a simple authentication process. Sets the state that is being loaded when a login request is initiated.

For simplicity, this process consists of three possible states:

The states on the diagram can be represented by the following state machines, including load state and authentication state:

When the login request is in progress, we disable the login button and display a progress indicator.

This sample app shows how to handle load state using various state management schemes.

The main navigation

The main navigation of the login page is through a widget that uses the Drawer menu to select from among different options.

The code is as follows:

class SignInPageNavigation extends StatelessWidget {
  const SignInPageNavigation({Key key, this.option}) : super(key: key);
  final ValueNotifier<Option> option;

  Option get _option => option.value;
  OptionData get _optionData => optionsData[_option];

  void _onSelectOption(Option selectedOption) {
    option.value = selectedOption;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_optionData.title),
      ),
      drawer: MenuSwitcher(
        options: optionsData,
        selectedOption: _option,
        onSelected: _onSelectOption,
      ),
      body: _buildContent(context),
    );
  }

  Widget _buildContent(BuildContext context) {
    switch (_option) {
      case Option.vanilla:
        return SignInPageVanilla();
      case Option.setState:
        return SignInPageSetState();
      case Option.bloc:
        return SignInPageBloc.create(context);
      case Option.valueNotifier:
        return SignInPageValueNotifier.create(context);
      default:
        returnContainer(); }}}Copy the code

This widget shows this Scaffold:

  • AppBarIs the name of the selected project
  • Drawer uses a custom constructorMenuSwitcher
  • The body uses a switch statement to distinguish between pages

Reference Flow (Vanilla)

To enable login, we can start with a simple vanilla implementation with no loading state:

class SignInPageVanilla extends StatelessWidget {
  Future<void> _signInAnonymously(BuildContext context) async {
    try {
      final auth = Provider.of<AuthService>(context);
      await auth.signInAnonymously();
    } on PlatformException catch (e) {
      await PlatformExceptionAlertDialog(
        title: 'Login failed',
        exception: e,
      ).show(context); }}@override
  Widget build(BuildContext context) {
    return Center(
      child: SignInButton(
        text: 'login', onPressed: () => _signInAnonymously(context), ), ); }}Copy the code

The _signInAnonymously method is called when the SignInButton button is clicked.

The Provider is used to get the AuthService object and use it for login.

Reading notes

  • AuthServiceIs a simple encapsulation of Firebase Authentication. For details, please seeThis article.
  • The authentication status is handled by an ancestor widget that usesonAuthStateChangedTo decide which pages to display. I am inPrevious post”, introduced this point.

setState

The load state can be added to the implementation just described through the following process:

  • Convert our widget toStatefulWidget
  • Define a local state variable
  • Put the state in the build method
  • Update it before and after login

Here is the final code:

class SignInPageSetState extends StatefulWidget {
  @override
  _SignInPageSetStateState createState() => _SignInPageSetStateState();
}

class _SignInPageSetStateState extends State<SignInPageSetState> {
  bool _isLoading = false;

  Future<void> _signInAnonymously() async {
    try {
      setState(() => _isLoading = true);
      final auth = Provider.of<AuthService>(context);
      await auth.signInAnonymously();
    } on PlatformException catch (e) {
      await PlatformExceptionAlertDialog(
        title: 'Login failed',
        exception: e,
      ).show(context);
    } finally {
      setState(() => _isLoading = false); }}@override
  Widget build(BuildContext context) {
    return Center(
      child: SignInButton(
        text: 'login',
        loading: _isLoading,
        onPressed: _isLoading ? null: () => _signInAnonymously(), ), ); }}Copy the code

Important: Notice how we use the finally closure. This can be used to execute some code, whether or not an exception is thrown.

BLoC

The loading state can be represented by the value of stream in BLoC.

We need some additional sample code to set up:

class SignInBloc {
  final _loadingController = StreamController<bool> (); Stream<bool> get loadingStream => _loadingController.stream;

  void setIsLoading(boolloading) => _loadingController.add(loading); dispose() { _loadingController.close(); }}class SignInPageBloc extends StatelessWidget {
  const SignInPageBloc({Key key, @required this.bloc}) : super(key: key);
  final SignInBloc bloc;

  static Widget create(BuildContext context) {
    return Provider<SignInBloc>(
      builder: (_) => SignInBloc(),
      dispose: (_, bloc) => bloc.dispose(),
      child: Consumer<SignInBloc>(
        builder: (_, bloc, __) => SignInPageBloc(bloc: bloc),
      ),
    );
  }

  Future<void> _signInAnonymously(BuildContext context) async {
    try {
      bloc.setIsLoading(true);
      final auth = Provider.of<AuthService>(context);
      await auth.signInAnonymously();
    } on PlatformException catch (e) {
      await PlatformExceptionAlertDialog(
        title: 'Login failed',
        exception: e,
      ).show(context);
    } finally {
      bloc.setIsLoading(false); }}@override
  Widget build(BuildContext context) {
    return StreamBuilder<bool>(
      stream: bloc.loadingStream,
      initialData: false,
      builder: (context, snapshot) {
        final isLoading = snapshot.data;
        return Center(
          child: SignInButton(
            text: 'login',
            loading: isLoading,
            onPressed: isLoading ? null: () => _signInAnonymously(context), ), ); }); }}Copy the code

In a nutshell, this code:

  • useStreamController<bool>Add aSignInBlocIs used to handle the load state.
  • Through the staticcreateMethod Provider/Consumer, letSignInBlocYou can access our widget.
  • in_signInAnonymouslyMethod, by callingbloc.setIsLoading(value)Update the stream.
  • throughStreamBuilderTo check the loading status and use it to set the login button.

Notes about RxDart

The BehaviorSubject is a special Stream controller that allows us to access the last value of the stream synchronously.

As an alternative to BloC, we could use BehaviorSubject to track the loading status and update it as needed.

I’ll use the GitHub project to show you how.

ValueNotifier

ValueNotifier can be used to hold a value and notify its listeners when it changes.

The same process code is as follows:

class SignInPageValueNotifier extends StatelessWidget {
  const SignInPageValueNotifier({Key key, this.loading}) : super(key: key);
  final ValueNotifier<bool> loading;

  static Widget create(BuildContext context) {
    return ChangeNotifierProvider<ValueNotifier<bool>>(
      builder: (_) => ValueNotifier<bool> (false),
      child: Consumer<ValueNotifier<bool>>(
        builder: (_, ValueNotifier<bool> isLoading, __) =>
            SignInPageValueNotifier(
              loading: isLoading,
            ),
      ),
    );
  }

  Future<void> _signInAnonymously(BuildContext context) async {
    try {
      loading.value = true;
      final auth = Provider.of<AuthService>(context);
      await auth.signInAnonymously();
    } on PlatformException catch (e) {
      await PlatformExceptionAlertDialog(
        title: 'Login failed',
        exception: e,
      ).show(context);
    } finally {
      loading.value = false; }}@override
  Widget build(BuildContext context) {
    return Center(
      child: SignInButton(
        text: 'login',
        loading: loading.value,
        onPressed: loading.value ? null: () => _signInAnonymously(context), ), ); }}Copy the code

In the static create method, we use ValueNotifier

‘s ChangeNotifierProvider and Consumer, which give us a way to indicate the loading status and rebuild the widget when it changes.

ValueNotifier vs ChangeNotifier

ValueNotifier and ChangeNotifier are closely related.

In fact, ValueNotifier is a subclass of ChangeNotifier that implements ValueListenable

.

This is the implementation of ValueNotifier in the Flutter SDK:

/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);

  /// The current value stored in this notifier.
  ///
  /// When the value is replaced with something that is not equal to the old
  /// value as evaluated by the equality operator ==, this class notifies its
  /// listeners.
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value) ';
}
Copy the code

So when should we use ValueNotifier and when should we use ChangeNotifier?

  • If you need to rebuild the widget when a simple value changes, useValueNotifier.
  • If you want to be in thenotifyListeners()Call with more control, please useChangeNotifier.

Notes on ScopedModel

The ChangeNotifierProvider is very similar to the ScopedModel. In fact, they are almost the same:

  • ScopedModel↔ ︎ChangeNotifierProvider
  • ScopedModelDescendant↔ ︎Consumer

Therefore, if you are already using a Provider, you do not need the ScopedModel because the ChangeNotifierProvider provides the same functionality.

Final comparison

The three implementations above (setState, BLoC, ValueNotifier) are very similar, except that they handle the load state differently.

Here’s how they compare:

  • SetState ↔︎ Simplest code
  • BLoC ↔︎ Most codes
  • ValueNotifier ↔︎ Medium level

So the setState scheme is best for this example, because we need to deal with the individual states of the individual widgets.

When building your own application, you can evaluate which solution is more appropriate for each situation 😉

Small egg: Implement Drawer menu

Keeping track of currently selected options is also a state management issue:

I first implement it in the custom Drawer menu using local state variables and setState.

However, after logging in, the state is lost because drawers have been removed from the Widget tree.

In one scenario, I decided to use ChangeNotifierProvider

> in LandingPage to store state:

class LandingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Used to keep track of the selected option across sign-in events
    final authService = Provider.of<AuthService>(context);
    return ChangeNotifierProvider<ValueNotifier<Option>>(
      builder: (_) => ValueNotifier<Option>(Option.vanilla),
      child: StreamBuilder<User>(
        stream: authService.onAuthStateChanged,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.active) {
            User user = snapshot.data;
            if (user == null) {
              return Consumer<ValueNotifier<Option>>(
                builder: (_, ValueNotifier<Option> option, __) =>
                    SignInPageNavigation(option: option),
              );
            }
            return HomePage();
          } else {
            returnScaffold( body: Center( child: CircularProgressIndicator(), ), ); }},),); }}Copy the code

StreamBuilder is used here to control the user’s authentication status.

By wrapping it up with ChangeNotifierProvider

>, I was able to retain the selected Option even after removing SignInPageNavigation.

The summary is as follows:

  • StatefulWidget no longer remembers its own state after state is removed.
  • Using providers, you can choose where to store the state in the Widget tree.
  • This way, the state is preserved even if the widgets that use it are deleted.

ValueNotifier requires more code than setState. However, it can be used to remember the state by placing the appropriate Provider in the Widget tree.

The source code

Sample code for this tutorial can be found here:

  • State Management Comparison: [setState BLoC ❖ ValueNotifier]

All of these state management schemes are covered in depth in my Flutter & Firebase Udemy course. You can check out this link (for discounts) :

  • Flutter & Firebase: Build a Complete App for iOS & Android

Have fun coding!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.