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:
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
- Provided counter bloc
- Resolve counter bloc to update state
- Efficiently render state changes
- Perform increment action
- 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