We are going to build a counter app using bloc.
$ flutter create counter_app --emptyUse BlocObserverto observe state changes.Use BlocProviderto provide a bloc to its children.Use BlocBuilderto handle building the widget in response to new states.
We can use Bloc to separate presentation from business logic, Bloc attempts to make state changes predictable by regulating when a state change can occur and enforcing a single way to change state throughout an entire application.
Cubit
Cubit is a lightweight state management solution. It is a subset of the bloc package that does not rely on events and instead uses methods to emit new states.
Cubit(State initialState) : super(initialState);BlocBase
A Cubit is a class which extends BlocBase and can be extended to manage any types of state.
class CounterCubit extends Cubit<int> { CounterCubit(): super(0);}BlocBase is an interface for the core functionality implemented by both Bloc and Cubit.
BlocBase(this._state) {// ignore: invalid_use_of_protected_member _blocObserver.onCreate(this);}Initial State
Every Cubit requires an initial state which will be the state of the cubit before emit has been called. The current state of a cubit can be accessed via the state getter.
class CounterCubit extends Cubit<int> { CounterCubit(): super(0);void increment() => emit(state + 1);}A Cubitcan expose functions which can be invoked to trigger state changes.
States are the output of a Cubit and represent a part of the application's state.
When creating a Cubit, we need to define the type of state which the Cubit will be managing. For example, the state of the CounterCubit can be represented via an int but in more complex cases it might be necessary to use a class instead of a primitive type.
We need to specify the initial state when we create a Cubit, we can do this by calling super with the value of the initial state.
In the CounterCubit, we set the initial state to 0 internally but we can also allow the Cubit to be more flexible by accepting an external value.
class CounterCubit extends Cubit<int> { CounterCubit(int initialState): super(initialState);}This would allow us to instantiate CounterCubit instances with different initial states like:
final cubitA = CounterCubit(0); // state starts at 0final cubitB = CounterCubit(8); // state starts at 8UI components can be notified of states and redraw portions of themselves based on the current state.
Emit
Each Cubit has the ability to output a new state via emit.
class CounterCubit extends Cubit<int> { CounterCubit() : super(0);void increment() => emit(state + 1);}The CounterCubit is exposing a public method called increment which can be called externally to notify the CounterCubit to increment its state.
When increment is called, we can access the current state of the Cubit via the state getter and emit a new state by adding 1 to the current state.
The
emitmethod is protected, meaning it should only be used inside of aCubit.
We can use a Cubit in the main function and change the state of Cubit.
void main() {final cubit = CounterCubit();print(cubit.state); // 0 cubit.increment(); // increase the stateprint(cubit.state); // 1 cubit.close();}Stream
Cubit exposes a Stream which allows us to receive real-time state updates:
Future<void> main() async {final cubit = CounterCubit();final subscription = cubit.stream.listen(print); // 1 cubit.increment();await Future.delayed(Duration.zero);await subscription.cancel();await cubit.close();}Subscribe Cubit
We can subscribe to the CounterCubit and call print on each state change. We are then invoking the increment function which will emit a new state. Lastly, we are calling cancel on the subscription when we no longer want to receive updates and closing the Cubit.
await Future.delayed(Duration.zero)can be used to avoid canceling the subscription immediately.
Only subsequent state changes will be received when calling
listenon aCubit.
When a Cubit emits a new state, a Change occurs, we can observe all changes for a given Cubit by overriding onChange.
class CounterCubit extends Cubit<int> { CounterCubit(): super(0);void increment() => emit(state + 1);@overridevoid onChange(Change<int> change) {super.onChange(change);print(change); }}We can then interact with the Cubit and observe all changes output to the console.
void main() { CounterCubit() ..increment() ..close();}A
Changeoccurs just before the state of theCubitis updated.
A
Changeconsists of thecurrentStateand thenextState.
The above example would output:
Change { currentState: 0, nextState: 1 }One added bonus of using the bloc library is that we can have access to all Changes in one place. Even though in this application we only have one Cubit, it's fairly common in larger applications to have many Cubits managing different parts of the application's state.
Observe Cubit
If we want to be able to do something in response to all Changes we can simply create our own BlocObserver.
class SimpleBlocObserver extends BlocObserver{@overridevoid onChange(BlocBase bloc, Change change) {super.onChange(bloc, change);print('${bloc.runtimeType} $change'); }}All we need to do is extend
BlocObserverand override theonChangemethod.
In order to use the SimpleBlocObserver, we just need to tweak the main function:
void main() { Bloc.observer = SimpleObserver(); CounterCubit() ..increment() ..close();}The above snippet would then output:
CounterCubit Change { currentState: 0, nextState: 1 }Change { currentState: 0, nextState: 1 }The internal
onChangeoverride is called first, which callssuper.onChangenotifying theonChangein theBlocObserver.
In
BlocObserverwe have access to theCubitinstance in addition to theChangeitself.
Error Handling
Every Cubit has an addError method which can be used to indicate that an error has occurred.
class CounterCubit extends Cubit<int> { CounterCubit(): super(0);void increment() { addError(Exception('increment error!'), StackTrace.current); emit(state + 1); }@overridevoid onChange(Change<int> change) {super.onChange(change);print(change); }@overridevoid onError(Object error, StackTrace stackTrace) {print('$error, $stackTrace); super.onError(error, stackTrace); }}
onErrorcan be overridden within theCubitto handle all errors for a specificCubit.
class SimpleBlocObserver extends BlocObserver{@overridevoid onChange(BlocBase bloc, Change change) {super.onChange(bloc, change);print('${bloc.runtimeType} $change'); }@overridevoid onError(BlocBase bloc, Object error, StackTrace stackTrace) {print('${bloc.runtimeType} $error $stackTrace');super.onError(bloc, error, stackTrace); }}When we run the same program again, we should see the following output:
Exception: increment error!#0 CounterCubit.increment (file:///main.dart:7:56)#1 main (file:///main.dart:41:7)#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)CounterCubit Exception: increment error!#0 CounterCubit.increment (file:///main.dart:7:56)#1 main (file:///main.dart:41:7)#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)CounterCubit Change { currentState: 0, nextState: 1 }Change { currentState: 0, nextState: 1 }
夜雨聆风