Flutter State Management

2 minute read

Stateful Widgets

Usually fine for simple widgets whose state remains local to the widget.

void _onPressed() {
  setState(
    (){
      _submitted = true;
  })
}

BLoCs (buisness logic components)

BLoCs use streams of immutable objects to update state and decouple the UI from services.

Given a model (oversimplified for this example):

class SubmittedModel {
  SubmittedModel({this.submitted = false});

  final bool submitted;
  // ...
}

Define a BLoC which provides the logic around this model.

class SubmittedBloc {
  SubmittedBloc({required this.service})

  final MyService service;

  static SubmittedBloc create(BuildContext context) {
    final service = Provider.of<MyService>(
      context, 
      listen: false
    );
    return SubmittedBloc(service: service);
  }

  final StreamController<SubmittedModel> _modelController = 
      StreamController<SubmittedModel>();

  Stream<SubmittedModel> get modelStream => 
      _modelController.stream;

  SubmittedModel _model = SubmittedModel();

  void dispose() {
    _modelController.close();
  }

  Future<void> submit() async {
    updateWith(submitted: true);
    try {
      await service.submit("<some buisness logic>");
    } catch (e) {
      updateWith(submitted: false);
      rethrow;
    }
  }

  void updateWith({
    bool? submitted,
  }) {
    _model = SubmittedModel(
      submitted: submitted ?? _model.submitted,
    );
    _modelController.add(_model);
  }
}

Building a widget which delegates buisness logic to a bloc:

class MyForm extends StatelessWidget {
  MyForm({Key? key, required this.bloc}) : super(key: key);
  final SubmittedBloc bloc;

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

  void _submit() {
    try {
      await bloc.submit();
      // UI updates on success.
    } catch (e) {
      // UI exception handling.
    }
  }

  //...
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<SubmittedModel>(
      stream: bloc.modelStream,
      initialData: SubmittedModel(),
      builder: (context, snapshot) {
        return // ...
            onPressed: snapshot.submitted ? _submit : null,
            // ...
      }
    );
  }
}

Value Notifier

ValueNotifier is simpler than a BLoC, but does not handle services. It is a specialized implementation of ChangeNotifier meant for simple values.

We can configure a widget with a ValueNotifier and ChangeNotifierProvider.

class MyForm extends StatelessWidget {
  const MyForm({Key? key, required this.submitted}) 
    : super(key: key);

  final bool submitted;

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

  //...
  @override
  Widget build(BuildContext context) {
    return  // ...
            onPressed: submitted ? _submit : null,
            // ...
      }
    );
  }
}

Change Notifier

A BLoC uses streams of immutable models, but ChangeNotifier is used with a mutable model. Like BLoCs, change notifiers may interact with services.

Change notifiers are mixed in with a model:

class SubmittedModel with ChangeNotifier {
  SubmittedModel({
    required this.service, 
    this.submitted = false,
  });

  final MyService service;
  bool submitted;

  Future<void> submit() async {
    updateWith(submitted: true);
    try {
      await service.submit("<some buisness logic>");
    } catch (e) {
      updateWith(submitted: false);
      rethrow;
    }
  }
  
  void updateWith({bool? submitted}) {
    this.submitted = submitted ?? this.submitted;

    notifyListeners();
  }
}

We can configure a widget with a ChangeNotifier and ChangeNotifierProvider.

class MyForm extends StatelessWidget {
  const MyForm({Key? key, required this.model}) 
    : super(key: key);

  final SubmittedModel model;

  static Widget create(BuildContext context) {
    final service = Provider.of<MyService>(
      context, 
      listen: false,
    );
    return ChangeNotifierProvider<SubmittedModel>(
      create: (_) => SubmittedModel(
        service: service, 
        submitted: false,
      ),
      child: Consumer<SubmittedModel>(
        builder: (_, model, __) => MyForm(
          model: model,
        ),
      ),
    );
  }

  void _submit() {
    try {
      await model.submit();
      // UI updates on success.
    } catch (e) {
      // UI exception handling.
    }
  }

  //...
  @override
  Widget build(BuildContext context) {
    return  // ...
            onPressed: model.submitted ? _submit : null,
            // ...
      }
    );
  }
}