State Management:

Our Journey

About Me

What is State?

State is data that changes over the lifecycle of the app.

Why State Management?

There is (almost) always state in our applications therefore we are (almost) always managing state whether we know it or not.

  • How can I change my app's state?
  • How can I update the UI based on the state?
  • How can I share state between different parts of my app?
  • How can I maintain my app as it grows?
  • How can I separate logic from UI?
  • How can I reuse code?

Vanilla

(setState)

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        Text('$_counter'),
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Vanilla So Far...

  • How can I change my app's state? ✅
  • How can I update the UI based on the state? ✅
  • How can I share state between different parts of my app? ❓
  • How can I maintain my app as it grows? ❓
  • How can I separate logic from UI?❓
  • How can I reuse code?

Vanilla

(Sharing State)

Vanilla

(Sharing State Continued...)

Vanilla

(InheritedWidget)

class InheritedCounter extends InheritedWidget {
  final Map _counter = { 'val': 0 };
  final Widget child;

  InheritedCounter({ this.child }) : super(child: child);

  increment() { 
     _counter['val']++;
  }

  get counter => _counter['val'];

  @override
  bool updateShouldNotify(InheritedCounter oldWidget) => true;

  static InheritedCounter of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(InheritedCounter);
}

Vanilla

(InheritedWidget Continued...)

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => InheritedCounter(child: HomePage());
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {
        int counter = InheritedCounter.of(context).counter;
        Function increment = InheritedCounter.of(context).increment;
        return Column(
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$counter'),
            RaisedButton(
              onPressed: () => setState(() => increment()),
              child: Text('Increment'),
            ),
          ],
        );
      },
    );
  }
}

Vanilla

(Sharing State Revisited...)

Vanilla Conclusion

  • How can I change my app's state? ✅
  • How can I update the UI based on the state? ✅
  • How can I share state between different parts of my app?
  • How can I maintain my app as it grows? ❓
  • How can I separate logic from UI?❓
  • How can I reuse code?

BLoC

(RxDart)

BLoC stands for Business Logic Component

  • be moved to one or more BLoCs
  • be removed as much as possible from the Presentation Layer
  • rely on exclusive use of Sinks for input and Streams for output
  • remain platform independent
  • remain environment independent

Business Logic should:

BLoC

(RxDart Continued...)

  • Widgets send events to the BLoC via Sinks,
  • Widgets are notified by the BLoC via streams,
  • The logic which is implemented by the BLoC is none of their concern.

BLoC

(Implementation)

class CounterBloc {
  int _counter = 0;

  BehaviorSubject<int> _counterStateSubject;
  PublishSubject<void> _incrementEventSubject;

  Stream<int> get counter => _counterStateSubject.stream;
  Sink<void> get increment => _incrementEventSubject.sink;

  StreamSubscription<void> _incrementEventSubscription;

  CounterBloc() {
    _counterStateSubject = BehaviorSubject<int>.seeded(_counter);
    _incrementEventSubject = PublishSubject<void>();
    _incrementEventSubscription =
        _incrementEventSubject.listen(_handleIncrement);
  }

  void dispose() {
    _incrementEventSubscription.cancel();
    _incrementEventSubject.close();
    _counterStateSubject.close();
  }

  void _handleIncrement(_) {
    _counterStateSubject.add(++_counter);
  }
}

BLoC

(Implementation Continued...)

class _HomePageState extends State<HomePage> {
  final CounterBloc _counterBloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        StreamBuilder<int>(
          stream: _counterBloc.counter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            return Text('${snapshot.data}');
          },
        ),
        RaisedButton(
          onPressed: () => _counterBloc.increment.add(null);
          child: Text('Increment'),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _counterBloc.dispose();
    super.dispose();
  }
}

BLoC Conclusion

  • How can I change my app's state? ✅
  • How can I update the UI based on the state? ✅
  • How can I share state between different parts of my app?
  • How can I maintain my app as it grows?
  • How can I separate logic from UI?
  • How can I reuse code?
  • How can I reduce boilerplate?
  • How can I reduce complexity of ReactiveX? ❓
  • How can I enforce/record state changes? ❓

Bloc Library

A predictable state management library that helps implement the BLoC design pattern.

Bloc Library

(Core Concepts)

  • Events are the input to a Bloc. They are commonly UI events such as button presses. Events are added and then converted to States.
  • States are the output of a Bloc. UI components can listen to the stream of states and redraw portions of themselves based on the given state
  • Blocs take a stream of events as input and transform them into a stream of states as output.

BLoC

(RxDart Flashback...)

Bloc Library

(in action)

enum CounterEvent { increment }

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.increment:
        yield state + 1;
        break;
    }
  }
}

Bloc Library

(in action continued...)

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('You have pushed the button this many times:'),
        BlocBuilder<CounterBloc, int>(
          builder: (BuildContext context, int counter) {
            return Text('$counter'),
          },
        ),
        RaisedButton(
          onPressed: () => BlocProvider.of<CounterBloc>(context).add(CounterEvent.increment),
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Bloc Library

(BlocProvider)

BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of<T>(context). It is used as a DI widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.

BlocProvider<BlocA>(
  create: (context) => BlocA(),
  child: ChildA(),
);
// without extensions:
BlocProvider.of<BlocA>(context)

// with extensions:
context.bloc<BlocA>()

then from ChildA we can retrieve BlocA via:

Bloc Library So Far...

  • How can I change my app's state? ✅
  • How can I update the UI based on the state? ✅
  • How can I share state between different parts of my app?
  • How can I maintain my app as it grows?
  • How can I separate logic from UI?
  • How can I reuse code?
  • How can I reduce boilerplate? ✅
  • How can I reduce complexity of Streams/Subscriptions?
  • How can I enforce/record state changes? ❓

Bloc Library

(transitions)

  • Transitions occur when an Event is dispatched after mapEventToState has been called but before the Bloc's state has been updated. A Transition consists of the currentState, the event which was dispatched, and the nextState.

  • BlocSupervisor oversees Blocs and delegates to BlocDelegate.

  • BlocDelegate handles events from all Blocs which are delegated by the BlocSupervisor. Can be used to intercept all Bloc Transitions and all Bloc errors. It is a great way to handle logging/analytics as well as error handling universally.

Bloc Library

(BlocObserver)

class MyBlocObserver extends BlocObserver {
  @override
  void onError(Object error, StackTrace stacktrace) {
    super.onError(error, stacktrace);
    print(error);
  }

  @override
  void onTransition(Transition transition) {
    super.onTransition(transition);
    print(transition);
  }
  
  @override
  void onEvent(Object event) {
    super.onEvent(event);
    print(event);
  }
}

void main() {
  Bloc.observer = MyBlocObserver();
  runApp(MyApp());
}

Bloc Library

(Tooling)

Bloc Library Conclusion

  • How can I change my app's state? ✅
  • How can I update the UI based on the state? ✅
  • How can I share state between different parts of my app?
  • How can I maintain my app as it grows?
  • How can I separate logic from UI?
  • How can I reuse code?
  • How can I reduce boilerplate? ✅
  • How can I reduce complexity of Streams/Subscriptions?
  • How can I enforce/record state changes?

Options are good...

State Management: Our Journey

By Felix Angelov

State Management: Our Journey

Flutter Chicago Meetup

  • 2,758