Migration Guide
v10.0.0
Section titled “v10.0.0”package:bloc_test
Section titled “package:bloc_test”❗✨ Decouple blocTest
from BlocBase
Section titled “❗✨ Decouple blocTest from BlocBase”Rationale
Section titled “Rationale”blocTest
should use the core bloc interfaces when possible for increased
flexibility and reusability. Previously this wasn’t possible because BlocBase
implemented StateStreamableSource
which was not enough for blocTest
due to
the internal dependency on the emit
API.
package:hydrated_bloc
Section titled “package:hydrated_bloc”❗✨ Support WebAssembly
Section titled “❗✨ Support WebAssembly”Rationale
Section titled “Rationale”It was previously not possible to compile apps to wasm when using
hydrated_bloc
. In v10.0.0, the package was refactored to allow compiling to
wasm.
v9.x.x
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App());}
v10.x.x
void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App());}
v9.0.0
Section titled “v9.0.0”package:bloc
Section titled “package:bloc”❗🧹 Remove Deprecated APIs
Section titled “❗🧹 Remove Deprecated APIs”Summary
Section titled “Summary”BlocOverrides
removed in favor ofBloc.observer
andBloc.transformer
❗✨ Introduce new EmittableStateStreamableSource
Interface
Section titled “❗✨ Introduce new EmittableStateStreamableSource Interface”Rationale
Section titled “Rationale”package:bloc_test
was previously tightly coupled to BlocBase
. The
EmittableStateStreamableSource
interface was introduced in order to allow
blocTest
to be decoupled from the BlocBase
concrete implementation.
package:hydrated_bloc
Section titled “package:hydrated_bloc”✨ Reintroduce HydratedBloc.storage
API
Section titled “✨ Reintroduce HydratedBloc.storage API”Rationale
Section titled “Rationale”Refer to the rationale for reintroducing the Bloc.observer and Bloc.transformer overrides.
v8.x.x
Future<void> main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, );}
v9.0.0
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App());}
v8.1.0
Section titled “v8.1.0”package:bloc
Section titled “package:bloc”✨ Reintroduce Bloc.observer
and Bloc.transformer
APIs
Section titled “✨ Reintroduce Bloc.observer and Bloc.transformer APIs”Rationale
Section titled “Rationale”The BlocOverrides
API was introduced in v8.0.0 in an attempt to support
scoping bloc-specific configurations such as BlocObserver
, EventTransformer
,
and HydratedStorage
. In pure Dart applications, the changes worked well;
however, in Flutter applications the new API caused more problems than it
solved.
The BlocOverrides
API was inspired by similar APIs in Flutter/Dart:
Problems
While it wasn’t the primary reason for these changes, the BlocOverrides
API
introduced additional complexity for developers. In addition to increasing the
amount of nesting and lines of code needed to achieve the same effect, the
BlocOverrides
API required developers to have a solid understanding of
Zones in Dart.
Zones
are not a beginner-friendly concept and failure to understand how Zones
work could lead to the introduction of bugs (such as uninitialized observers,
transformers, storage instances).
For example, many developers would have something like:
void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...);}
The above code, while appearing harmless, can actually lead to many difficult to
track bugs. Whatever zone WidgetsFlutterBinding.ensureInitialized
is initially
called from will be the zone in which gesture events are handled (e.g. onTap
,
onPressed
callbacks) due to GestureBinding.initInstances
. This is just one
of many issues caused by using zoneValues
.
In addition, Flutter does many things behind the scenes which involve forking/manipulating Zones (especially when running tests) which can lead to unexpected behaviors (and in many cases behaviors that are outside the developer’s control — see issues below).
Due to the use of the
runZoned, the
transition to the BlocOverrides
API led to the discovery of several
bugs/limitations in Flutter (specifically around Widget and Integration Tests):
- https://github.com/flutter/flutter/issues/96939
- https://github.com/flutter/flutter/issues/94123
- https://github.com/flutter/flutter/issues/93676
which affected many developers using the bloc library:
- https://github.com/felangel/bloc/issues/3394
- https://github.com/felangel/bloc/issues/3350
- https://github.com/felangel/bloc/issues/3319
v8.0.x
void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), );}
v8.1.0
void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer();
// ...}
v8.0.0
Section titled “v8.0.0”package:bloc
Section titled “package:bloc”❗✨ Introduce new BlocOverrides
API
Section titled “❗✨ Introduce new BlocOverrides API”Rationale
Section titled “Rationale”The previous API used to override the default BlocObserver
and
EventTransformer
relied on a global singleton for both the BlocObserver
and
EventTransformer
.
As a result, it was not possible to:
- Have multiple
BlocObserver
orEventTransformer
implementations scoped to different parts of the application - Have
BlocObserver
orEventTransformer
overrides be scoped to a package- If a package were to depend on
package:bloc
and registered its ownBlocObserver
, any consumer of the package would either have to overwrite the package’sBlocObserver
or report to the package’sBlocObserver
.
- If a package were to depend on
It was also more difficult to test because of the shared global state across tests.
Bloc v8.0.0 introduces a BlocOverrides
class which allows developers to
override BlocObserver
and/or EventTransformer
for a specific Zone
rather
than relying on a global mutable singleton.
v7.x.x
void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer();
// ...}
v8.0.0
void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), );}
Bloc
instances will use the BlocObserver
and/or EventTransformer
for the
current Zone
via BlocOverrides.current
. If there are no BlocOverrides
for
the zone, they will use the existing internal defaults (no change in
behavior/functionality).
This allows allow each Zone
to function independently with its own
BlocOverrides
.
BlocOverrides.runZoned( () { // BlocObserverA and eventTransformerA final overrides = BlocOverrides.current;
// Blocs in this zone report to BlocObserverA // and use eventTransformerA as the default transformer. // ...
// Later... BlocOverrides.runZoned( () { // BlocObserverB and eventTransformerB final overrides = BlocOverrides.current;
// Blocs in this zone report to BlocObserverB // and use eventTransformerB as the default transformer. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(),);
❗✨ Improve Error Handling and Reporting
Section titled “❗✨ Improve Error Handling and Reporting”Rationale
Section titled “Rationale”The goal of these changes is:
- make internal unhandled exceptions extremely obvious while still preserving bloc functionality
- support
addError
without disrupting control flow
Previously, error handling and reporting varied depending on whether the
application was running in debug or release mode. In addition, errors reported
via addError
were treated as uncaught exceptions in debug mode which led to a
poor developer experience when using the addError
API (specifically when
writing unit tests).
In v8.0.0, addError
can be safely used to report errors and blocTest
can be
used to verify that errors are reported. All errors are still reported to
onError
, however, only uncaught exceptions are rethrown (regardless of debug
or release mode).
❗🧹 Make BlocObserver
abstract
Section titled “❗🧹 Make BlocObserver abstract”Rationale
Section titled “Rationale”BlocObserver
was intended to be an interface. Since the default API
implementation are no-ops, BlocObserver
is now an abstract
class to clearly
communicate that the class is meant to be extended and not directly
instantiated.
v7.x.x
void main() { // It was possible to create an instance of the base class. final observer = BlocObserver();}
v8.0.0
class MyBlocObserver extends BlocObserver {...}
void main() { // Cannot instantiate the base class. final observer = BlocObserver(); // ERROR
// Extend `BlocObserver` instead. final observer = MyBlocObserver(); // OK}
❗✨ add
throws StateError
if Bloc is closed
Section titled “❗✨ add throws StateError if Bloc is closed”Rationale
Section titled “Rationale”Previously, it was possible to call add
on a closed bloc and the internal
error would get swallowed, making it difficult to debug why the added event was
not being processed. In order to make this scenario more visible, in v8.0.0,
calling add
on a closed bloc will throw a StateError
which will be reported
as an uncaught exception and propagated to onError
.
❗✨ emit
throws StateError
if Bloc is closed
Section titled “❗✨ emit throws StateError if Bloc is closed”Rationale
Section titled “Rationale”Previously, it was possible to call emit
within a closed bloc and no state
change would occur but there would also be no indication of what went wrong,
making it difficult to debug. In order to make this scenario more visible, in
v8.0.0, calling emit
within a closed bloc will throw a StateError
which will
be reported as an uncaught exception and propagated to onError
.
❗🧹 Remove Deprecated APIs
Section titled “❗🧹 Remove Deprecated APIs”Summary
Section titled “Summary”mapEventToState
removed in favor ofon<Event>
transformEvents
removed in favor ofEventTransformer
APITransitionFunction
typedef removed in favor ofEventTransformer
APIlisten
removed in favor ofstream.listen
package:bloc_test
Section titled “package:bloc_test”✨ MockBloc
and MockCubit
no longer require registerFallbackValue
Section titled “✨ MockBloc and MockCubit no longer require registerFallbackValue”Summary
Section titled “Summary”registerFallbackValue
is only needed when using the any()
matcher from
package:mocktail
for a custom type. Previously, registerFallbackValue
was
needed for every Event
and State
when using MockBloc
or MockCubit
.
v8.x.x
class FakeMyEvent extends Fake implements MyEvent {}class FakeMyState extends Fake implements MyState {}class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); });
// Tests...}
v9.0.0
class MyMockBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}
void main() { // Tests...}
package:hydrated_bloc
Section titled “package:hydrated_bloc”❗✨ Introduce new HydratedBlocOverrides
API
Section titled “❗✨ Introduce new HydratedBlocOverrides API”Rationale
Section titled “Rationale”Previously, a global singleton was used to override the Storage
implementation.
As a result, it was not possible to have multiple Storage
implementations
scoped to different parts of the application. It was also more difficult to test
because of the shared global state across tests.
HydratedBloc
v8.0.0 introduces a HydratedBlocOverrides
class which allows
developers to override Storage
for a specific Zone
rather than relying on a
global mutable singleton.
v7.x.x
void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), );
// ...}
v8.0.0
void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), );
HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, );}
HydratedBloc
instances will use the Storage
for the current Zone
via
HydratedBlocOverrides.current
.
This allows allow each Zone
to function independently with its own
BlocOverrides
.
v7.2.0
Section titled “v7.2.0”package:bloc
Section titled “package:bloc”✨ Introduce new on<Event>
API
Section titled “✨ Introduce new on<Event> API”Rationale
Section titled “Rationale”The on<Event>
API was introduced as part of
[Proposal] Replace mapEventToState with on<Event> in Bloc.
Due to an issue in Dart it’s
not always obvious what the value of state
will be when dealing with nested
async generators (async*
). Even though there are ways to work around the
issue, one of the core principles of the bloc library is to be predictable. The
on<Event>
API was created to make the library as safe as possible to use and
to eliminate any uncertainty when it comes to state changes.
Summary
on<E>
allows you to register an event handler for all events of type E
. By
default, events will be processed concurrently when using on<E>
as opposed to
mapEventToState
which processes events sequentially
.
v7.1.0
abstract class CounterEvent {}class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0);
@override Stream<int> mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } }}
v7.2.0
abstract class CounterEvent {}class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); }}
If you want to retain the exact same behavior as in v7.1.0 you can register a
single event handler for all events and apply a sequential
transformer:
import 'package:bloc/bloc.dart';import 'package:bloc_concurrency/bloc_concurrency.dart';
class MyBloc extends Bloc<MyEvent, MyState> { MyBloc() : super(MyState()) { on<MyEvent>(_onEvent, transformer: sequential()) }
FutureOr<void> _onEvent(MyEvent event, Emitter<MyState> emit) async { // TODO: logic goes here... }}
You can also override the default EventTransformer
for all blocs in your
application:
import 'package:bloc/bloc.dart';import 'package:bloc_concurrency/bloc_concurrency.dart';
void main() { Bloc.transformer = sequential<dynamic>(); ...}
✨ Introduce new EventTransformer
API
Section titled “✨ Introduce new EventTransformer API”Rationale
Section titled “Rationale”The on<Event>
API opened the door to being able to provide a custom event
transformer per event handler. A new EventTransformer
typedef was introduced
which enables developers to transform the incoming event stream for each event
handler rather than having to specify a single event transformer for all events.
Summary
An EventTransformer
is responsible for taking the incoming stream of events
along with an EventMapper
(your event handler) and returning a new stream of
events.
typedef EventTransformer<Event> = Stream<Event> Function(Stream<Event> events, EventMapper<Event> mapper)
The default EventTransformer
processes all events concurrently and looks
something like:
EventTransformer<E> concurrent<E>() { return (events, mapper) => events.flatMap(mapper);}
v7.1.0
@overrideStream<Transition<MyEvent, MyState>> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn);}
v7.2.0
/// Define a custom `EventTransformer`EventTransformer<MyEvent> debounce<MyEvent>(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper);}
MyBloc() : super(MyState()) { /// Apply the custom `EventTransformer` to the `EventHandler` on<MyEvent>(_onEvent, transformer: debounce(const Duration(milliseconds: 300)))}
⚠️ Deprecate transformTransitions
API
Section titled “⚠️ Deprecate transformTransitions API”Rationale
Section titled “Rationale”The stream
getter on Bloc
makes it easy to override the outbound stream of
states therefore it’s no longer valuable to maintain a separate
transformTransitions
API.
Summary
v7.1.0
@overrideStream<Transition<Event, State>> transformTransitions( Stream<Transition<Event, State>> transitions,) { return transitions.debounceTime(const Duration(milliseconds: 42));}
v7.2.0
@overrideStream<State> get stream => super.stream.debounceTime(const Duration(milliseconds: 42));
v7.0.0
Section titled “v7.0.0”package:bloc
Section titled “package:bloc”❗ Bloc and Cubit extend BlocBase
Section titled “❗ Bloc and Cubit extend BlocBase”Rationale
Section titled “Rationale”As a developer, the relationship between blocs and cubits was a bit awkward. When cubit was first introduced it began as the base class for blocs which made sense because it had a subset of the functionality and blocs would just extend Cubit and define additional APIs. This came with a few drawbacks:
-
All APIs would either have to be renamed to accept a cubit for accuracy or they would need to be kept as bloc for consistency even though hierarchically it is inaccurate (#1708, #1560).
-
Cubit would need to extend Stream and implement EventSink in order to have a common base which widgets like BlocBuilder, BlocListener, etc. can be implemented against (#1429).
Later, we experimented with inverting the relationship and making bloc the base class which partially resolved the first bullet above but introduced other issues:
- The cubit API is bloated due to the underlying bloc APIs like mapEventToState,
add, etc. (#2228)
- Developers can technically invoke these APIs and break things
- We still have the same issue of cubit exposing the entire stream API as before (#1429)
To address these issues we introduced a base class for both Bloc
and Cubit
called BlocBase
so that upstream components can still interoperate with both
bloc and cubit instances but without exposing the entire Stream
and
EventSink
API directly.
Summary
BlocObserver
v6.1.x
class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...}
@override void onEvent(Bloc bloc, Object event) {...}
@override void onChange(Cubit cubit, Object event) {...}
@override void onTransition(Bloc bloc, Transition transition) {...}
@override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...}
@override void onClose(Cubit cubit) {...}}
v7.0.0
class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...}
@override void onEvent(Bloc bloc, Object event) {...}
@override void onChange(BlocBase bloc, Object? event) {...}
@override void onTransition(Bloc bloc, Transition transition) {...}
@override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...}
@override void onClose(BlocBase bloc) {...}}
Bloc/Cubit
v6.1.x
final bloc = MyBloc();bloc.listen((state) {...});
final cubit = MyCubit();cubit.listen((state) {...});
v7.0.0
final bloc = MyBloc();bloc.stream.listen((state) {...});
final cubit = MyCubit();cubit.stream.listen((state) {...});
package:bloc_test
Section titled “package:bloc_test”❗seed returns a function to support dynamic values
Section titled “❗seed returns a function to support dynamic values”Rationale
Section titled “Rationale”In order to support having a mutable seed value which can be updated dynamically
in setUp
, seed
returns a function.
Summary
v7.x.x
blocTest( '...', seed: MyState(), ...);
v8.0.0
blocTest( '...', seed: () => MyState(), ...);
❗expect returns a function to support dynamic values and includes matcher support
Section titled “❗expect returns a function to support dynamic values and includes matcher support”Rationale
Section titled “Rationale”In order to support having a mutable expectation which can be updated
dynamically in setUp
, expect
returns a function. expect
also supports
Matchers
.
Summary
v7.x.x
blocTest( '...', expect: [MyStateA(), MyStateB()], ...);
v8.0.0
blocTest( '...', expect: () => [MyStateA(), MyStateB()], ...);
// It can also be a `Matcher`blocTest( '...', expect: () => contains(MyStateA()), ...);
❗errors returns a function to support dynamic values and includes matcher support
Section titled “❗errors returns a function to support dynamic values and includes matcher support”Rationale
Section titled “Rationale”In order to support having a mutable errors which can be updated dynamically in
setUp
, errors
returns a function. errors
also supports Matchers
.
Summary
v7.x.x
blocTest( '...', errors: [MyError()], ...);
v8.0.0
blocTest( '...', errors: () => [MyError()], ...);
// It can also be a `Matcher`blocTest( '...', errors: () => contains(MyError()), ...);
❗MockBloc and MockCubit
Section titled “❗MockBloc and MockCubit”Rationale
Section titled “Rationale”To support stubbing of various core APIs, MockBloc
and MockCubit
are
exported as part of the bloc_test
package. Previously, MockBloc
had to be
used for both Bloc
and Cubit
instances which was not intuitive.
Summary
v7.x.x
class MockMyBloc extends MockBloc<MyState> implements MyBloc {}class MockMyCubit extends MockBloc<MyState> implements MyBloc {}
v8.0.0
class MockMyBloc extends MockBloc<MyEvent, MyState> implements MyBloc {}class MockMyCubit extends MockCubit<MyState> implements MyCubit {}
❗Mocktail Integration
Section titled “❗Mocktail Integration”Rationale
Section titled “Rationale”Due to various limitations of the null-safe
package:mockito described
here,
package:mocktail is used by MockBloc
and
MockCubit
. This allows developers to continue using a familiar mocking API
without the need to manually write stubs or rely on code generation.
Summary
v7.x.x
import 'package:mockito/mockito.dart';
...
when(bloc.state).thenReturn(MyState());verify(bloc.add(any)).called(1);
v8.0.0
import 'package:mocktail/mocktail.dart';
...
when(() => bloc.state).thenReturn(MyState());verify(() => bloc.add(any())).called(1);
Please refer to #347 as well as the mocktail documentation for more information.
package:flutter_bloc
Section titled “package:flutter_bloc”❗ rename cubit
parameter to bloc
Section titled “❗ rename cubit parameter to bloc”Rationale
Section titled “Rationale”As a result of the refactor in package:bloc
to introduce BlocBase
which
Bloc
and Cubit
extend, the parameters of BlocBuilder
, BlocConsumer
, and
BlocListener
were renamed from cubit
to bloc
because the widgets operate
on the BlocBase
type. This also further aligns with the library name and
hopefully improves readability.
Summary
v6.1.x
BlocBuilder( cubit: myBloc, ...)
BlocListener( cubit: myBloc, ...)
BlocConsumer( cubit: myBloc, ...)
v7.0.0
BlocBuilder( bloc: myBloc, ...)
BlocListener( bloc: myBloc, ...)
BlocConsumer( bloc: myBloc, ...)
package:hydrated_bloc
Section titled “package:hydrated_bloc”❗storageDirectory is required when calling HydratedStorage.build
Section titled “❗storageDirectory is required when calling HydratedStorage.build”Rationale
Section titled “Rationale”In order to make package:hydrated_bloc
a pure Dart package, the dependency on
package:path_provider was removed and
the storageDirectory
parameter when calling HydratedStorage.build
is
required and no longer defaults to getTemporaryDirectory
.
Summary
v6.x.x
HydratedBloc.storage = await HydratedStorage.build();
v7.0.0
import 'package:path_provider/path_provider.dart';
...
HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(),);
v6.1.0
Section titled “v6.1.0”package:flutter_bloc
Section titled “package:flutter_bloc”❗context.bloc and context.repository are deprecated in favor of context.read and context.watch
Section titled “❗context.bloc and context.repository are deprecated in favor of context.read and context.watch”Rationale
Section titled “Rationale”context.read
, context.watch
, and context.select
were added to align with
the existing provider API which many
developers are familiar and to address issues that have been raised by the
community. To improve the safety of the code and maintain consistency,
context.bloc
was deprecated because it can be replaced with either
context.read
or context.watch
dependending on if it’s used directly within
build
.
context.watch
context.watch
addresses the request to have a
MultiBlocBuilder because we can
watch several blocs within a single Builder
in order to render UI based on
multiple states:
Builder( builder: (context) { final stateA = context.watch<BlocA>().state; final stateB = context.watch<BlocB>().state; final stateC = context.watch<BlocC>().state;
// return a Widget which depends on the state of BlocA, BlocB, and BlocC });
context.select
context.select
allows developers to render/update UI based on a part of a bloc
state and addresses the request to have a
simpler buildWhen.
final name = context.select((UserBloc bloc) => bloc.state.user.name);
The above snippet allows us to access and rebuild the widget only when the current user’s name changes.
context.read
Even though it looks like context.read
is identical to context.bloc
there
are some subtle but significant differences. Both allow you to access a bloc
with a BuildContext
and do not result in rebuilds; however, context.read
cannot be called directly within a build
method. There are two main reasons to
use context.bloc
within build
:
- To access the bloc’s state
@overrideWidget build(BuildContext context) { final state = context.bloc<MyBloc>().state; return Text('$state');}
The above usage is error prone because the Text
widget will not be rebuilt if
the state of the bloc changes. In this scenario, either a BlocBuilder
or
context.watch
should be used.
@overrideWidget build(BuildContext context) { final state = context.watch<MyBloc>().state; return Text('$state');}
or
@overrideWidget build(BuildContext context) { return BlocBuilder<MyBloc, MyState>( builder: (context, state) => Text('$state'), );}
- To access the bloc so that an event can be added
@overrideWidget build(BuildContext context) { final bloc = context.bloc<MyBloc>(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... )}
The above usage is inefficient because it results in a bloc lookup on each
rebuild when the bloc is only needed when the user taps the ElevatedButton
. In
this scenario, prefer to use context.read
to access the bloc directly where it
is needed (in this case, in the onPressed
callback).
@overrideWidget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read<MyBloc>().add(MyEvent()), ... )}
Summary
v6.0.x
@overrideWidget build(BuildContext context) { final bloc = context.bloc<MyBloc>(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... )}
v6.1.x
@overrideWidget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read<MyBloc>().add(MyEvent()), ... )}
?> If accessing a bloc to add an event, perform the bloc access using
context.read
in the callback where it is needed.
v6.0.x
@overrideWidget build(BuildContext context) { final state = context.bloc<MyBloc>().state; return Text('$state');}
v6.1.x
@overrideWidget build(BuildContext context) { final state = context.watch<MyBloc>().state; return Text('$state');}
?> Use context.watch
when accessing the state of the bloc in order to ensure
the widget is rebuilt when the state changes.
v6.0.0
Section titled “v6.0.0”package:bloc
Section titled “package:bloc”❗BlocObserver onError takes Cubit
Section titled “❗BlocObserver onError takes Cubit”Rationale
Section titled “Rationale”Due to the integration of Cubit
, onError
is now shared between both Bloc
and Cubit
instances. Since Cubit
is the base, BlocObserver
will accept a
Cubit
type rather than a Bloc
type in the onError
override.
v5.x.x
class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); }}
v6.0.0
class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); }}
❗Bloc does not emit last state on subscription
Section titled “❗Bloc does not emit last state on subscription”Rationale
Section titled “Rationale”This change was made to align Bloc
and Cubit
with the built-in Stream
behavior in Dart
. In addition, conforming this the old behavior in the context
of Cubit
led to many unintended side-effects and overall complicated the
internal implementations of other packages such as flutter_bloc
and
bloc_test
unnecessarily (requiring skip(1)
, etc…).
v5.x.x
final bloc = MyBloc();bloc.listen(print);
Previously, the above snippet would output the initial state of the bloc followed by subsequent state changes.
v6.x.x
In v6.0.0, the above snippet does not output the initial state and only outputs subsequent state changes. The previous behavior can be achieved with the following:
final bloc = MyBloc();print(bloc.state);bloc.listen(print);
?> Note: This change will only affect code that relies on direct bloc
subscriptions. When using BlocBuilder
, BlocListener
, or BlocConsumer
there
will be no noticeable change in behavior.
package:bloc_test
Section titled “package:bloc_test”❗MockBloc only requires State type
Section titled “❗MockBloc only requires State type”Rationale
Section titled “Rationale”It is not necessary and eliminates extra code while also making MockBloc
compatible with Cubit
.
v5.x.x
class MockCounterBloc extends MockBloc<CounterEvent, int> implements CounterBloc {}
v6.0.0
class MockCounterBloc extends MockBloc<int> implements CounterBloc {}
❗whenListen only requires State type
Section titled “❗whenListen only requires State type”Rationale
Section titled “Rationale”It is not necessary and eliminates extra code while also making whenListen
compatible with Cubit
.
v5.x.x
whenListen<CounterEvent,int>(bloc, Stream.fromIterable([0, 1, 2, 3]));
v6.0.0
whenListen<int>(bloc, Stream.fromIterable([0, 1, 2, 3]));
❗blocTest does not require Event type
Section titled “❗blocTest does not require Event type”Rationale
Section titled “Rationale”It is not necessary and eliminates extra code while also making blocTest
compatible with Cubit
.
v5.x.x
blocTest<CounterBloc, CounterEvent, int>( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const <int>[1],);
v6.0.0
blocTest<CounterBloc, int>( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const <int>[1],);
❗blocTest skip defaults to 0
Section titled “❗blocTest skip defaults to 0”Rationale
Section titled “Rationale”Since bloc
and cubit
instances will no longer emit the latest state for new
subscriptions, it was no longer necessary to default skip
to 1
.
v5.x.x
blocTest<CounterBloc, CounterEvent, int>( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const <int>[0],);
v6.0.0
blocTest<CounterBloc, int>( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const <int>[],);
The initial state of a bloc or cubit can be tested with the following:
test('initial state is correct', () { expect(MyBloc().state, InitialState());});
❗blocTest make build synchronous
Section titled “❗blocTest make build synchronous”Rationale
Section titled “Rationale”Previously, build
was made async
so that various preparation could be done
to put the bloc under test in a specific state. It is no longer necessary and
also resolves several issues due to the added latency between the build and the
subscription internally. Instead of doing async prep to get a bloc in a desired
state we can now set the bloc state by chaining emit
with the desired state.
v5.x.x
blocTest<CounterBloc, CounterEvent, int>( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const <int>[2],);
v6.0.0
blocTest<CounterBloc, int>( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const <int>[2],);
package:flutter_bloc
Section titled “package:flutter_bloc”❗BlocBuilder bloc parameter renamed to cubit
Section titled “❗BlocBuilder bloc parameter renamed to cubit”Rationale
Section titled “Rationale”In order to make BlocBuilder
interoperate with bloc
and cubit
instances
the bloc
parameter was renamed to cubit
(since Cubit
is the base class).
v5.x.x
BlocBuilder( bloc: myBloc, builder: (context, state) {...})
v6.0.0
BlocBuilder( cubit: myBloc, builder: (context, state) {...})
❗BlocListener bloc parameter renamed to cubit
Section titled “❗BlocListener bloc parameter renamed to cubit”Rationale
Section titled “Rationale”In order to make BlocListener
interoperate with bloc
and cubit
instances
the bloc
parameter was renamed to cubit
(since Cubit
is the base class).
v5.x.x
BlocListener( bloc: myBloc, listener: (context, state) {...})
v6.0.0
BlocListener( cubit: myBloc, listener: (context, state) {...})
❗BlocConsumer bloc parameter renamed to cubit
Section titled “❗BlocConsumer bloc parameter renamed to cubit”Rationale
Section titled “Rationale”In order to make BlocConsumer
interoperate with bloc
and cubit
instances
the bloc
parameter was renamed to cubit
(since Cubit
is the base class).
v5.x.x
BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...})
v6.0.0
BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...})
v5.0.0
Section titled “v5.0.0”package:bloc
Section titled “package:bloc”❗initialState has been removed
Section titled “❗initialState has been removed”Rationale
Section titled “Rationale”As a developer, having to override initialState
when creating a bloc presents
two main issues:
- The
initialState
of the bloc can be dynamic and can also be referenced at a later point in time (even outside of the bloc itself). In some ways, this can be viewed as leaking internal bloc information to the UI layer. - It’s verbose.
v4.x.x
class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState => 0;
...}
v5.0.0
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0);
...}
?> For more information check out #1304
❗BlocDelegate renamed to BlocObserver
Section titled “❗BlocDelegate renamed to BlocObserver”Rationale
Section titled “Rationale”The name BlocDelegate
was not an accurate description of the role that the
class played. BlocDelegate
suggests that the class plays an active role
whereas in reality the intended role of the BlocDelegate
was for it to be a
passive component which simply observes all blocs in an application.
v4.x.x
class MyBlocDelegate extends BlocDelegate { ...}
v5.0.0
class MyBlocObserver extends BlocObserver { ...}
❗BlocSupervisor has been removed
Section titled “❗BlocSupervisor has been removed”Rationale
Section titled “Rationale”BlocSupervisor
was yet another component that developers had to know about and
interact with for the sole purpose of specifying a custom BlocDelegate
. With
the change to BlocObserver
we felt it improved the developer experience to set
the observer directly on the bloc itself.
?> This changed also enabled us to decouple other bloc add-ons like
HydratedStorage
from the BlocObserver
.
v4.x.x
BlocSupervisor.delegate = MyBlocDelegate();
v5.0.0
Bloc.observer = MyBlocObserver();
package:flutter_bloc
Section titled “package:flutter_bloc”❗BlocBuilder condition renamed to buildWhen
Section titled “❗BlocBuilder condition renamed to buildWhen”Rationale
Section titled “Rationale”When using BlocBuilder
, we previously could specify a condition
to determine
whether the builder
should rebuild.
BlocBuilder<MyBloc, MyState>( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...})
The name condition
is not very self-explanatory or obvious and more
importantly, when interacting with a BlocConsumer
the API became inconsistent
because developers can provide two conditions (one for builder
and one for
listener
). As a result, the BlocConsumer
API exposed a buildWhen
and
listenWhen
BlocConsumer<MyBloc, MyState>( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...},)
In order to align the API and provide a more consistent developer experience,
condition
was renamed to buildWhen
.
v4.x.x
BlocBuilder<MyBloc, MyState>( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...})
v5.0.0
BlocBuilder<MyBloc, MyState>( buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...})
❗BlocListener condition renamed to listenWhen
Section titled “❗BlocListener condition renamed to listenWhen”Rationale
Section titled “Rationale”For the same reasons as described above, the BlocListener
condition was also
renamed.
v4.x.x
BlocListener<MyBloc, MyState>( condition: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...})
v5.0.0
BlocListener<MyBloc, MyState>( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...})
package:hydrated_bloc
Section titled “package:hydrated_bloc”❗HydratedStorage and HydratedBlocStorage renamed
Section titled “❗HydratedStorage and HydratedBlocStorage renamed”Rationale
Section titled “Rationale”In order to improve code reuse between
hydrated_bloc and
hydrated_cubit, the concrete default
storage implementation was renamed from HydratedBlocStorage
to
HydratedStorage
. In addition, the HydratedStorage
interface was renamed from
HydratedStorage
to Storage
.
v4.0.0
class MyHydratedStorage implements HydratedStorage { ...}
v5.0.0
class MyHydratedStorage implements Storage { ...}
❗HydratedStorage decoupled from BlocDelegate
Section titled “❗HydratedStorage decoupled from BlocDelegate”Rationale
Section titled “Rationale”As mentioned earlier, BlocDelegate
was renamed to BlocObserver
and was set
directly as part of the bloc
via:
Bloc.observer = MyBlocObserver();
The following change was made to:
- Stay consistent with the new bloc observer API
- Keep the storage scoped to just
HydratedBloc
- Decouple the
BlocObserver
fromStorage
v4.0.0
BlocSupervisor.delegate = await HydratedBlocDelegate.build();
v5.0.0
HydratedBloc.storage = await HydratedStorage.build();
❗Simplified Initialization
Section titled “❗Simplified Initialization”Rationale
Section titled “Rationale”Previously, developers had to manually call
super.initialState ?? DefaultInitialState()
in order to setup their
HydratedBloc
instances. This is clunky and verbose and also incompatible with
the breaking changes to initialState
in bloc
. As a result, in v5.0.0
HydratedBloc
initialization is identical to normal Bloc
initialization.
v4.0.0
class CounterBloc extends HydratedBloc<CounterEvent, int> { @override int get initialState => super.initialState ?? 0;}
v5.0.0
class CounterBloc extends HydratedBloc<CounterEvent, int> { CounterBloc() : super(0);
...}