Dash Tips: Using Freezed with Bloc

Justification

If you need control flow and middleware as part of your state, bloc is an outstanding option. Coupling it with freezed enhances applications with generated ==/toString methods and sealed unions. The overriden toString is great for debugging and the overridden == ensures efficiency. Finally, sealed unions are great for enforcing hard rules over the soft conventions of traditional conditional statements such as if or switch.

The Counter Bloc

Let's build our counter bloc. For the sake of brevity, events & states will be included in the same file.

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

part 'counter.freezed.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  @override
  get initialState => CounterState.initial();

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    yield event.when(
      increment: () => CounterState.current(state.value + 1),
      decrement: () => CounterState.current(state.value - 1),
    );
  }

  void increment() => this.add(CounterEvent.increment());
  void decrement() => this.add(CounterEvent.decrement());
}

@freezed
abstract class CounterEvent with _$CounterEvent {
  const factory CounterEvent.increment() = _Increment;
  const factory CounterEvent.decrement() = _Decrement;
}

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

Before we can run our project, we will need to build our generated classes.

flutter pub run build_runner watch --delete-conflicting-outputs

Running this command in the project root should resolve any errors that you were seeing in your editor. You can see that our bloc does some peculiar things in comparison to the official documentation:

1. The states and events are created using nested sealed classes.

Using nested sealed classes ensures that individual subclasses cannot be instantiated individually. This enforces consistency with how developers interact with blocs.

2. The current counter value is resolved with maybeMap a function.

The maybeMap function is generated by freezed. It allows us to map only the states that we care about, with a fallback for everything else.

3. Events are handled using when function, not a switch statement.

This last feature is particularly interesting. If we were using switch or if statements to map our events, there is a good chance that we could eventually add more events and forget to handle them. This could lead to unwanted behavior in our application. Using the when function ensures that we must handle every case. Think of it as an exhaustive switch statement. If we don't handle every case, our IDE will bark at us:

Screen Shot 2020-04-08 at 10.11.43 PM.png

The Counter Page

All that's left is to build our Counter Page.

import 'package:bloc_with_freezed/blocs/counter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 bloc
      home: BlocProvider<CounterBloc>(
        create: (context) => CounterBloc(),
        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 bloc to update state
    final counterBloc = context.bloc<CounterBloc>();
    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
            BlocBuilder<CounterBloc, CounterState>(
              builder: (_, 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: () => counterBloc.increment(),
            ),
          ),
          Padding(
            padding: fabPadding,
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              ///5. Perform decrement action
              onPressed: () => counterBloc.decrement(),
            ),
          ),
        ],
      ),
    );
  }
}

Highlighting the important areas in this example, we

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

The final example should look very similar to the official documentation with one caveat where we render the counter:

state.when(
  current: (value) => Text('$value', style: textStyle),
  initial: (value) => Text('$value', style: textStyle),
)

Just like in our bloc, we can ensure that every state is handled by using the when function generated for us by freezed. The result is an elegant solution for mapping bloc events to states and states to widgets!

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

No Comments Yet