docs/src/content/docs/bloc-concepts.mdx
import CountStreamSnippet from '/components/concepts/bloc/CountStreamSnippet.astro';
import SumStreamSnippet from '/components/concepts/bloc/SumStreamSnippet.astro';
import StreamsMainSnippet from '/components/concepts/bloc/StreamsMainSnippet.astro';
import CounterCubitSnippet from '/components/concepts/bloc/CounterCubitSnippet.astro';
import CounterCubitInitialStateSnippet from '/components/concepts/bloc/CounterCubitInitialStateSnippet.astro';
import CounterCubitInstantiationSnippet from '/components/concepts/bloc/CounterCubitInstantiationSnippet.astro';
import CounterCubitIncrementSnippet from '/components/concepts/bloc/CounterCubitIncrementSnippet.astro';
import CounterCubitBasicUsageSnippet from '/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro';
import CounterCubitStreamUsageSnippet from '/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro';
import CounterCubitOnChangeSnippet from '/components/concepts/bloc/CounterCubitOnChangeSnippet.astro';
import CounterCubitOnChangeUsageSnippet from '/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro';
import CounterCubitOnChangeOutputSnippet from '/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro';
import SimpleBlocObserverOnChangeSnippet from '/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro';
import SimpleBlocObserverOnChangeUsageSnippet from '/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro';
import SimpleBlocObserverOnChangeOutputSnippet from '/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro';
import CounterCubitOnErrorSnippet from '/components/concepts/bloc/CounterCubitOnErrorSnippet.astro';
import SimpleBlocObserverOnErrorSnippet from '/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro';
import CounterCubitOnErrorOutputSnippet from '/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro';
import CounterBlocSnippet from '/components/concepts/bloc/CounterBlocSnippet.astro';
import CounterBlocEventHandlerSnippet from '/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro';
import CounterBlocIncrementSnippet from '/components/concepts/bloc/CounterBlocIncrementSnippet.astro';
import CounterBlocUsageSnippet from '/components/concepts/bloc/CounterBlocUsageSnippet.astro';
import CounterBlocStreamUsageSnippet from '/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro';
import CounterBlocOnChangeSnippet from '/components/concepts/bloc/CounterBlocOnChangeSnippet.astro';
import CounterBlocOnChangeUsageSnippet from '/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro';
import CounterBlocOnChangeOutputSnippet from '/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro';
import CounterBlocOnTransitionSnippet from '/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro';
import CounterBlocOnTransitionOutputSnippet from '/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro';
import SimpleBlocObserverOnTransitionSnippet from '/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro';
import SimpleBlocObserverOnTransitionUsageSnippet from '/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro';
import SimpleBlocObserverOnTransitionOutputSnippet from '/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro';
import CounterBlocOnEventSnippet from '/components/concepts/bloc/CounterBlocOnEventSnippet.astro';
import SimpleBlocObserverOnEventSnippet from '/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro';
import SimpleBlocObserverOnEventOutputSnippet from '/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro';
import CounterBlocOnErrorSnippet from '/components/concepts/bloc/CounterBlocOnErrorSnippet.astro';
import CounterBlocOnErrorOutputSnippet from '/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro';
import CounterCubitFullSnippet from '/components/concepts/bloc/CounterCubitFullSnippet.astro';
import CounterBlocFullSnippet from '/components/concepts/bloc/CounterBlocFullSnippet.astro';
import AuthenticationStateSnippet from '/components/concepts/bloc/AuthenticationStateSnippet.astro';
import AuthenticationTransitionSnippet from '/components/concepts/bloc/AuthenticationTransitionSnippet.astro';
import AuthenticationChangeSnippet from '/components/concepts/bloc/AuthenticationChangeSnippet.astro';
import DebounceEventTransformerSnippet from '/components/concepts/bloc/DebounceEventTransformerSnippet.astro';
:::note
Please make sure to carefully read the following sections before working with
package:bloc.
:::
There are several core concepts that are critical to understanding how to use the bloc package.
In the upcoming sections, we're going to discuss each of them in detail as well as work through how they would apply to a counter app.
:::note
Check out the official
Dart Documentation for more
information about Streams.
:::
A stream is a sequence of asynchronous data.
In order to use the bloc library, it is critical to have a basic understanding
of Streams and how they work.
If you're unfamiliar with Streams just think of a pipe with water flowing
through it. The pipe is the Stream and the water is the asynchronous data.
We can create a Stream in Dart by writing an async* (async generator)
function.
By marking a function as async* we are able to use the yield keyword and
return a Stream of data. In the above example, we are returning a Stream of
integers up to the max integer parameter.
Every time we yield in an async* function we are pushing that piece of data
through the Stream.
We can consume the above Stream in several ways. If we wanted to write a
function to return the sum of a Stream of integers it could look something
like:
By marking the above function as async we are able to use the await keyword
and return a Future of integers. In this example, we are awaiting each value
in the stream and returning the sum of all integers in the stream.
We can put it all together like so:
<StreamsMainSnippet />Now that we have a basic understanding of how Streams work in Dart we're ready
to learn about the core component of the bloc package: a Cubit.
A Cubit is a class which extends BlocBase and can be extended to manage any
type of state.
A Cubit can expose functions which can be invoked to trigger state changes.
States are the output of a Cubit and represent a part of your application's
state. UI components can be notified of states and redraw portions of themselves
based on the current state.
:::note
For more information about the origins of Cubit checkout
the following issue.
:::
We can create a CounterCubit like:
When creating a Cubit, we need to define the type of state which the Cubit
will be managing. In the case of the CounterCubit above, the state can be
represented via an int but in more complex cases it might be necessary to use
a class instead of a primitive type.
The second thing we need to do when creating a Cubit is specify the initial
state. We can do this by calling super with the value of the initial state. In
the snippet above, we are setting the initial state to 0 internally but we can
also allow the Cubit to be more flexible by accepting an external value:
This would allow us to instantiate CounterCubit instances with different
initial states like:
Each Cubit has the ability to output a new state via emit.
In the above snippet, 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.
:::caution
The emit method is protected, meaning it should only be used inside of a
Cubit.
:::
We can now take the CounterCubit we've implemented and put it to use!
In the above snippet, we start by creating an instance of the CounterCubit. We
then print the current state of the cubit which is the initial state (since no
new states have been emitted yet). Next, we call the increment function to
trigger a state change. Finally, we print the state of the Cubit again which
went from 0 to 1 and call close on the Cubit to close the internal state
stream.
Cubit exposes a Stream which allows us to receive real-time state updates:
In the above snippet, we are subscribing to the CounterCubit and calling 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.
:::note
await Future.delayed(Duration.zero) is added for this example to avoid
canceling the subscription immediately.
:::
:::caution
Only subsequent state changes will be received when calling listen on a
Cubit.
:::
When a Cubit emits a new state, a Change occurs. We can observe all changes
for a given Cubit by overriding onChange.
We can then interact with the Cubit and observe all changes output to the
console.
The above example would output:
<CounterCubitOnChangeOutputSnippet />:::note
A Change occurs just before the state of the Cubit is updated. A Change
consists of the currentState and the nextState.
:::
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.
If we want to be able to do something in response to all Changes we can simply
create our own BlocObserver.
:::note
All we need to do is extend BlocObserver and override the onChange method.
:::
In order to use the SimpleBlocObserver, we just need to tweak the main
function:
The above snippet would then output:
<SimpleBlocObserverOnChangeOutputSnippet />:::note
The internal onChange override is called first, which calls super.onChange
notifying the onChange in the BlocObserver.
:::
:::tip
In BlocObserver we have access to the Cubit instance in addition to the
Change itself.
:::
Every Cubit has an addError method which can be used to indicate that an
error has occurred.
:::note
onError can be overridden within the Cubit to handle all errors for a
specific Cubit.
:::
onError can also be overridden in BlocObserver to handle all reported errors
globally.
If we run the same program again we should see the following output:
<CounterCubitOnErrorOutputSnippet />A Bloc is a more advanced class which relies on events to trigger state
changes rather than functions. Bloc also extends BlocBase which means it has
a similar public API as Cubit. However, rather than calling a function on a
Bloc and directly emitting a new state, Blocs receive events and convert
the incoming events into outgoing states.
Creating a Bloc is similar to creating a Cubit except in addition to
defining the state that we'll be managing, we must also define the event that
the Bloc will be able to process.
Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.
<CounterBlocSnippet />Just like when creating the CounterCubit, we must specify an initial state by
passing it to the superclass via super.
Bloc requires us to register event handlers via the on<Event> API, as
opposed to functions in Cubit. An event handler is responsible for converting
any incoming events into zero or more outgoing states.
:::tip
An EventHandler has access to the added event as well as an Emitter which
can be used to emit zero or more states in response to the incoming event.
:::
We can then update the EventHandler to handle the CounterIncrementPressed
event:
In the above snippet, we have registered an EventHandler to manage all
CounterIncrementPressed events. For each incoming CounterIncrementPressed
event we can access the current state of the bloc via the state getter and
emit(state + 1).
:::note
Since the Bloc class extends BlocBase, we have access to the current state
of the bloc at any point in time via the state getter just like in Cubit.
:::
:::caution
Blocs should never directly emit new states. Instead every state change must
be output in response to an incoming event within an EventHandler.
:::
:::caution
Both blocs and cubits will ignore duplicate states. If we emit State nextState
where state == nextState, then no state change will occur.
:::
At this point, we can create an instance of our CounterBloc and put it to use!
In the above snippet, we start by creating an instance of the CounterBloc. We
then print the current state of the Bloc which is the initial state (since no
new states have been emitted yet). Next, we add the CounterIncrementPressed
event to trigger a state change. Finally, we print the state of the Bloc again
which went from 0 to 1 and call close on the Bloc to close the internal
state stream.
:::note
await Future.delayed(Duration.zero) is added to ensure we wait for the next
event-loop iteration (allowing the EventHandler to process the event).
:::
Just like with Cubit, a Bloc is a special type of Stream, which means we
can also subscribe to a Bloc for real-time updates to its state:
In the above snippet, we are subscribing to the CounterBloc and calling print
on each state change. We are then adding the CounterIncrementPressed event
which triggers the on<CounterIncrementPressed> EventHandler and emits a new
state. Lastly, we are calling cancel on the subscription when we no longer
want to receive updates and closing the Bloc.
:::note
await Future.delayed(Duration.zero) is added for this example to avoid
canceling the subscription immediately.
:::
Since Bloc extends BlocBase, we can observe all state changes for a Bloc
using onChange.
We can then update main.dart to:
Now if we run the above snippet, the output will be:
<CounterBlocOnChangeOutputSnippet />One key differentiating factor between Bloc and Cubit is that because Bloc
is event-driven, we are also able to capture information about what triggered
the state change.
We can do this by overriding onTransition.
The change from one state to another is called a Transition. A Transition
consists of the current state, the event, and the next state.
If we then rerun the same main.dart snippet from before, we should see the
following output:
:::note
onTransition is invoked before onChange and contains the event which
triggered the change from currentState to nextState.
:::
Just as before, we can override onTransition in a custom BlocObserver to
observe all transitions that occur from a single place.
We can initialize the SimpleBlocObserver just like before:
Now if we run the above snippet, the output should look like:
<SimpleBlocObserverOnTransitionOutputSnippet />:::note
onTransition is invoked first (local before global) followed by onChange.
:::
Another unique feature of Bloc instances is that they allow us to override
onEvent which is called whenever a new event is added to the Bloc. Just like
with onChange and onTransition, onEvent can be overridden locally as well
as globally.
We can run the same main.dart as before and should see the following output:
:::note
onEvent is called as soon as the event is added. The local onEvent is
invoked before the global onEvent in BlocObserver.
:::
Just like with Cubit, each Bloc has an addError and onError method. We
can indicate that an error has occurred by calling addError from anywhere
inside our Bloc. We can then react to all errors by overriding onError just
as with Cubit.
If we rerun the same main.dart as before, we can see what it looks like when
an error is reported:
:::note
The local onError is invoked first followed by the global onError in
BlocObserver.
:::
:::note
onError and onChange work the exact same way for both Bloc and Cubit
instances.
:::
:::caution
Any unhandled exceptions that occur within an EventHandler are also reported
to onError.
:::
Now that we've covered the basics of the Cubit and Bloc classes, you might
be wondering when you should use Cubit and when you should use Bloc.
One of the biggest advantages of using Cubit is simplicity. When creating a
Cubit, we only have to define the state as well as the functions which we want
to expose to change the state. In comparison, when creating a Bloc, we have to
define the states, events, and the EventHandler implementation. This makes
Cubit easier to understand and there is less code involved.
Now let's take a look at the two counter implementations:
The Cubit implementation is more concise and instead of defining events
separately, the functions act like events. In addition, when using a Cubit, we
can simply call emit from anywhere in order to trigger a state change.
One of the biggest advantages of using Bloc is knowing the sequence of state
changes as well as exactly what triggered those changes. For state that is
critical to the functionality of an application, it might be very beneficial to
use a more event-driven approach in order to capture all events in addition to
state changes.
A common use case might be managing AuthenticationState. For simplicity, let's
say we can represent AuthenticationState via an enum:
There could be many reasons as to why the application's state could change from
authenticated to unauthenticated. For example, the user might have tapped a
logout button and requested to be signed out of the application. On the other
hand, maybe the user's access token was revoked and they were forcefully logged
out. When using Bloc we can clearly trace how the application state got to a
certain state.
The above Transition gives us all the information we need to understand why
the state changed. If we had used a Cubit to manage the AuthenticationState,
our logs would look like:
This tells us that the user was logged out but it doesn't explain why which might be critical to debugging and understanding how the state of the application is changing over time.
Another area in which Bloc excels over Cubit is when we need to take
advantage of reactive operators such as buffer, debounceTime, throttle,
etc.
:::tip
See package:stream_transform and
package:rxdart for stream transformers.
:::
Bloc has an event sink that allows us to control and transform the incoming
flow of events.
For example, if we were building a real-time search, we would probably want to debounce the requests to the backend in order to avoid getting rate-limited as well as to cut down on cost/load on the backend.
With Bloc we can provide a custom EventTransformer to change the way
incoming events are processed by the Bloc.
With the above code, we can easily debounce the incoming events with very little additional code.
:::tip
Check out
package:bloc_concurrency for an
opinionated set of event transformers.
:::
If you are unsure about which to use, start with Cubit and you can later
refactor or scale-up to a Bloc as needed.