Migration Guide
این محتوا هنوز به زبان شما در دسترس نیست.
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());}
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();
// ...}
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(),);
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).
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}
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
.
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
.
mapEventToState
removed in favor ofon<Event>
transformEvents
removed in favor ofEventTransformer
APITransitionFunction
typedef removed in favor ofEventTransformer
APIlisten
removed in favor ofstream.listen
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...}
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
.
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>(); ...}
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)))}
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));
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) {...});
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(), ...);
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()), ...);
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()), ...);
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 {}
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.
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, ...)
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(),);
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.
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); }}
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.
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 {}
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]));
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],);
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());});
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],);
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) {...})
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) {...})
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) {...})
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
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
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();
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) {...})
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) {...})
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 { ...}
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();
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);
...}