Dash Tips: Using Freezed with StateNotifier

Justification

Previously I wrote about using freezed with the awesome bloc library. Now let's look at the same example implemented with state_notifier to get a good understanding of the differences.

The Counter State Notifier

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:state_notifier/state_notifier.dart';

part 'counter.freezed.dart';

class CounterStateNotifier extends StateNotifier<CounterState>
    with LocatorMixin {
  CounterStateNotifier() : super(CounterState.initial());

  void increment() {
    state = CounterState.current(state.value + 1);
  }

  void decrement() {
    state = CounterState.current(state.value + 1);
  }
}

@freezed
abstract class CounterState with _$CounterState {
  const factory CounterState.initial([@Default(0) int value]) = _Initial;
  const factory CounterState.current(int value) = _Current;
}

You might notice that we have shaved ~30% of the code to basically accomplish the same thing. While this example is a bit contrived, it highlights an important fact: solutions of the event sourcing ilk are not free, they inherently come with more boilerplate because of the additional features that they provide. We are trading simplicity for features.

The Counter Page

All that's left is to build our Counter Page. Our changes are almost negligible.

import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:freezed_with_statenotifier/controller/counter.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      ///1. Provided counter state notifier
      home: StateNotifierProvider<CounterStateNotifier, CounterState>(
        create: (context) => CounterStateNotifier(),
        child: HomePage(title: 'Counter Demo'),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  HomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    ///2. Resolve counter state notifier to update state
    var counterStateNotifier = Provider.of<CounterStateNotifier>(context);
    final textStyle = Theme.of(context).textTheme.display1;
    final fabPadding = EdgeInsets.symmetric(vertical: 5.0);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            ///3. Efficiently render state changes
            StateNotifierBuilder(
              stateNotifier: counterStateNotifier,
              builder: (context, state, _) => state.when(
                current: (value) => Text('$value', style: textStyle),
                initial: (value) => Text('$value', style: textStyle),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: fabPadding,
            child: FloatingActionButton(
              child: Icon(Icons.add),
              ///4. Perform increment action
              onPressed: () => counterStateNotifier.increment(),
            ),
          ),
          Padding(
            padding: fabPadding,
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              ///5. Perform decrement action
              onPressed: () => counterStateNotifier.decrement(),
            ),
          ),
        ],
      ),
    );
  }
}

Similar to our bloc example, in the state notifier example we

  1. Provided counter state notifier
  2. Resolve counter state notifier to update state
  3. Efficiently render state changes
  4. Perform increment action
  5. Perform decrement action

But why StateNotifier?

As good citizens, we

  • Should not over-optimize.
  • Should not use patterns or libraries because they’re popular.

If you are wondering "Why would I use this library vs. library x?" That's a great question. For the uninitiated, I'm a big proponent of yagni when it comes to application architecture. If your project does not require control flow, centralized middleware, or any other features generally associated with event sourcing, then you are probably better off not using bloc.

Sup?! I’m Ryan Edge. I am a Software Engineer at Superformula and a semi-pro open source contributor. If you liked this article, be sure to follow and spread the love! Happy trails

Remi Rousselet's photo

Features like middlewares are still doable here though:

The setter of "state" is overridable.

Ryan Edge's photo

Most definitely! StateNotifier can be scaled up to match Bloc capabilities. That point is more to highlight reasons Bloc may be overkill.

Žan Fras's photo

How to add something like loading state?

I tried:

@freezed
abstract class CounterState with _$CounterState {
  const factory CounterState.initial([@Default(0) int value]) = _Initial;
  const factory CounterState.loading() = _Loading;
  const factory CounterState.current(int value) = _Current;
}

Of course I get an error if I try to use it in notifier, because current looses it:

The getter 'value' isn't defined for the type 'CounterState'.

How to go about it?

Thanks

Žan Fras's photo

What I did (to present loading state example) is:

@freezed
abstract class CounterState with _$CounterState {
  const factory CounterState.initial([@Default(0) int value]) = _Initial;
  const factory CounterState.loading(int value) = _Loading;
  const factory CounterState.current(int value) = _Current;
}
class CounterStateNotifier extends StateNotifier<CounterState>
    with LocatorMixin {
  CounterStateNotifier() : super(CounterState.initial());

  void increment() {
    state = CounterState.loading(state.value);
    Timer(
      Duration(seconds: 1),
      () => state = CounterState.current(state.value + 1),
    );
  }
}
StateNotifierBuilder(
              stateNotifier: context.watch<CounterStateNotifier>(),
              builder: (context, CounterState state, _) => state.when(
                initial: (value) => Text(
                  '$value',
                  style: Theme.of(context).textTheme.headline4,
                ),
                loading: (_) => CircularProgressIndicator(),
                current: (value) => Text(
                  '$value',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            )

But that is obviously 'hack'. Any tip? Remi Rousselet / Ryan Edge