Flutter State Management

Ken Baldauf

Senior Software Engineer @ Originate

State Management Techniques

  • Callbacks
  • InheritedWidget
  • Redux
  • Provider & Scoped Model
  • BLoC
  • MobX

Recap of Flutter Basics

  • Almost everything (including alignment, padding & layout) is a widget
  • A widget's main job is to provide a build method that describes how to display the widget
  • Such build methods are often described in terms of other, lower-level widgets

StatelessWidget

  • Are immutable and non-dynamic
  • Depends only on the data passed into its constructor
  • Examples:
    • Text
    • Button
    • Icon

StatefulWidget

  • Are immutable, but dynamic
  • Owns an instance of a State class
    • State persists over the lifetime of the widget
  • Examples:
    • Form
    • Checkbox
    • Image

State

  • Flutter is declarative
    • It builds the UI to reflect the app's current state
    • UI redraws when the app state changes
  • State can be changed via setState method

Local State Example

class MyHomepage extends StatefulWidget {
  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}

How to Share State?

  • Local state is self contained with a single StatefulWidget
  • State Management
  • Simple
    • Callbacks
    • InheritedWidget
  • Advanced
    • Redux
    • Provider
    • Scoped Model
    • BLoC
    • MobX

Lifting State Up

  • Keep state above widgets that use it

Callbacks

@override
Widget build(BuildContext context) {
  return SomeWidget(
    // Construct the widget, passing it a reference to the method above.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}
  • Can work for simple things
  • Gets messy fast

InheritedWidget

  • Allows propagation of data down the widget tree
  • Data obtained by a child with a build context
  • Basic building block for more complex solutions
  • Follows convention of providing a static method: of
  • Utilized by various built-in Flutter classes
    • Ex: Theme.of returns app's ThemeData

InheritedWidget Example

class CounterState extends InheritedWidget {
  CounterState({Key key, this.count, this.incrementer, this.decrementer, Widget child})
      : super(key: key, child: child);

  final int count;
  final Function incrementer;
  final Function decrementer;

  @override
  bool updateShouldNotify(CounterState oldWidget) {
    return count != oldWidget.count;
  }

  static of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(CounterState);
  }
}

InheritedWidget Example

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
  int count = 0;
  void incrementer() {
    setState(() {
      count++;
    });
  }
  void decrementer() {
    setState(() {
      count--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CounterState(
      count: count,
      incrementer: incrementer,
      decrementer: decrementer,
      child: HomePage(),
    );
  }
}

InheritedWidget Example

class CountSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterState = CounterState.of(context);
    return new Text('${counterState.count}');
  }
}

class ButtonSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterState = CounterState.of(context);
    return new Column(
      children: <Widget>[
        ButtonBar(
          children: <Widget>[
            RaisedButton(child: Text('incrementer'), 
            	onPressed: counterState.incrementer,),
            RaisedButton(child: Text('decrementer'), 
            	onPressed: counterState.decrementer,),
          ],
        ),
      ],
    );
  }
}

Redux

  • Store
    • single source of truth for state
  • Action
    • dispatched to change app state
  • Reducer
    • take current state + action
    • return new state
  • StoreProvider
    • allows store to be passed to descendants
  • StoreConnector
    • allows widget to read or change state

Redux Example

// actions
class IncrementCountAction {}
class DecrememtCountAction {}

// reducer
int counterReducer(int currentCount, action) {
  if (action is IncrementCountAction) {
    currentCount++;
    return currentCount;
  } else if (action is DecrememtCountAction) {
    currentCount--;
    return currentCount;
  } else {
    return currentCount;
  }
}

// app state
class AppState {
  final int count;

  AppState({this.count = 0});
}

Redux Example

// app reducer
AppState appReducer(AppState state, action) {
  return new AppState(
      count: counterReducer(state.count, action),
  );
}

// main app
class MainApp extends StatelessWidget {
  final store = new Store<AppState>(
    appReducer,
    initialState: new AppState(),
    middleware: [],
  );

  @override
  Widget build(BuildContext context) {
    return new StoreProvider(
      store: store,
      child: new MaterialApp(
          title: 'title',
          home: new HomePage(),
      ),
    );
  }
}

Redux Example

class IncreaseCountButton extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return new StoreConnector<AppState, VoidCallback>(
      converter: (Store<AppState> store) {
        return () {
          store.dispatch(new IncrementCountAction());
        };
      },
      builder: (BuildContext context, VoidCallback increase) {
        return new FloatingActionButton(
          onPressed: increase,
          child: new Icon(Icons.add),
        );
      },
    );
  }
}

Redux Example

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new StoreConnector<AppState, _ViewModel>(
        converter: _ViewModel.fromStore,
        builder: (BuildContext context, _ViewModel vm) {
          return new Text(vm.count.toString());
        }
    );
  }
}

class _ViewModel {
  final int count;

  _ViewModel({
    @required this.count,
  });

  static _ViewModel fromStore(Store<AppState> store) {
    return new _ViewModel(count: store.state.count);
  }
}

Provider

  • ChangeNotifier
    • form of an Observable
  • ChangeNotifierProvider
    • widget that provides a ChangeNotifier to descendants
    • MultiProvider allows providing multiple CNs
  • Consumer
    • widget that consumes a specific CN type
  • Provider.of
    • allows updating a CN without listening to changes
    • Provider can be used for other dependency injection needs

Provider Example

void main() {
  runApp(
    // Provide the model to all widgets within the app
    ChangeNotifierProvider(
      // Initialize the model in the builder
      builder: (context) => Counter(),
      child: MyHomePage(),
    ),
  );
}

class Counter with ChangeNotifier {
  int value = 0;

  void increment() {
    value += 1;
    notifyListeners();
  }
}

Provider Example

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Consumer<Counter>(
              builder: (context, counter, child) => Text(
                '${counter.value}',
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =>
            Provider.of<Counter>(context, listen: false).increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

Scoped Model

  • Consolidate app wide state & business logic
  • Minimum boiler plate code
  • Similar to Provider
    • only for state management
  • Consists of 3 parts:
    • Model
    • ScopedModel
      • holds state variables & business logic
    • ScopedModelDescendant
      • allows widgets to update on ScopedModel changes

Scoped Model Example

// Model
class Counter {
  int value = 0;
}

// ScopedModel
import 'package:scoped_model/scoped_model.dart';

class ScopedCounter extends Model {
  Counter counter = Counter();

  increment() {
    counter.value += 1;
    notifyListeners();
  }
}

Scoped Model Example

class MyHomePage extends StatelessWidget {
  final ScopedCounter scopedCounter = ScopedCounter();

  @override
  Widget build(BuildContext context) {
    return ScopedModel(
      model: scopedCounter,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Hello World with Scoped Model'),
        ),
        body: Center(
          child: WidgetCounter(),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => scopedCounter.increment(),
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

Scoped Model Example

class WidgetCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant(
        builder: (context, child, model) =>
            Text('WidgetCounter counter is ${model.counter.count}'));
  }
}

ScopedModel.of<ScopedCounter>(context, rebuildOnChange: false)

BLoC

  • Business Logic Component
  • Acts as a Presenter/ViewModel
  • flutter_bloc
    • optional 3rd party lib
    • offers providers & builders
    • meant to make BLoC architecture easier to utilize
  • Utilizes Dart's async streams API
    • StreamController
      • add data via sink
      • listen for changes via stream
  • Can also make use of RxDart
    • PublishSubject, ReplaySubject & BehaviourSubject

BLoC Example

class CounterProvider {
  int count = 0;
  void increaseCount() => count++;
}

class CounterBloc {
  final counterController = StreamController();
  final CounterProvider provider = CounterProvider();
  
  Stream get getCount => counterController.stream;
  // the rxdart stream controllers returns an Observable
  
  void updateCount() {
    provider.increaseCount();
    counterController.sink.add(provider.count);
  }
  
  void dispose() {
    // close our StreamController to avoid memory leak
    counterController.close(); 
  }
}

final bloc = CounterBloc();

BLoC Example

// import file that contains our bloc
class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: bloc.getCount,
      initialData: CounterProvider().count,
      builder: (context, snapshot) => Center(
            child: Column(
              children: <Widget>[
                Text('You have pushed the button this many times:'),
                Text('${snapshot.data}'),
              ],
            ),
          ),
    );
  }
}

MobX

  • JS framework just recently came to Flutter
  • Similar in concepts to BLoC
  • Observable
    • represents reactive state of the app
    • boilerplate can be reduced with mobx_codegen
  • Computed Observable
    • derived from the value of other observables
  • Action
    • used to mutate observables
  • Reaction
    • notified when observable they track changes
    • ex: Observer, autorun, when, ayncwhen, etc. 

MobX Example

class Counter {
  Counter() {
    increment = Action(_increment);
  }

  final _value = Observable(0);
  int get value => _value.value;

  set value(int newValue) => _value.value = newValue;
  Action increment;

  void _increment() {
    _value.value++;
  }
}
part 'counter.g.dart';
// _$Counter comes from generated code included above
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

MobX Example

class CounterWidget extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<CounterWidget> {
  final Counter counter = Counter();

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('MobX Counter')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('You have pushed the button this many times:'),
              Observer(builder: (_) => Text('${counter.value}')),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: counter.increment,
          child: const Icon(Icons.add),
        ),
      );
}

Conclusions

  • Flutter community has yet to reach a consensus
    • Redux, Scoped Model & BLoC are most popular
  • Use Scoped Model over Provider
    • unless you're also using Provider's other features
  • MobX for Dart/Flutter is still in its infancy
    • MobX's code generator is really nice
  • Scoped Model
    • easy to learn & arguably the best starting point
  • BLoC
    • streams are similar to Android's LiveData
    • larger learning curve & may be overkill for simple apps
    • has potential to be more powerful than Scoped Model

Questions?

Flutter State Management

By Kenneth Baldauf

Flutter State Management

Flutter is Google's cross platform mobile application framework; let's take a look and see at some of advanced state management techniques.

  • 508