From 0937ca064b53838b78ed83ccf41b82247a1abb7c Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Fri, 11 Apr 2025 11:21:30 +0930 Subject: [PATCH] feat(flutter_bloc): support listener current state Allows setting a `startWithCurrentState` flag on `BlocListener` which changes the behaviour to immediately call `listener` with the current `state` of the bloc before calling again with subsequent emissions. This allows the view layer to react to whatever the current state is rather than only when it changes. For example: ```dart return BlocListener( // Ensure the listener is checked immediately on creation with // current state in addition to future transitions. // If the user is somehow routed to this page when not logged in // they should be immediately kicked out, rather than only when // they transition from logged in to logged out. startWithCurrentState: true, listener: (context, state) { if(!state.isLoggedIn) { context.router.replace(const UnauthenticatedRoute()); } }, child: MyAuthenticatedPageContent(), ); ``` Closes #4347 --- .../flutter_bloc/lib/src/bloc_listener.dart | 53 ++++++++++++++++++- .../flutter_bloc/test/bloc_listener_test.dart | 47 ++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/flutter_bloc/lib/src/bloc_listener.dart b/packages/flutter_bloc/lib/src/bloc_listener.dart index a4df5013850..c588bae29fa 100644 --- a/packages/flutter_bloc/lib/src/bloc_listener.dart +++ b/packages/flutter_bloc/lib/src/bloc_listener.dart @@ -74,6 +74,31 @@ typedef BlocListenerCondition = bool Function(S previous, S current); /// ) /// ``` /// {@endtemplate} +/// +/// {@template bloc_listener_start_with_current_state} +/// By default, the [listener] will only be called on new state emissions from +/// the bloc. If you want to call the listener with the current state +/// immediately, set [startWithCurrentState] to true. +/// Note that [listenWhen] is not called for the current state (as there is no +/// previous state to compare to). +/// +/// ```dart +/// // Default behavior +/// BlocListener( +/// listener: (context, state) { +/// // this is only called when BlocA emits new states +/// }, +/// ) +/// +/// BlocListener( +/// startWithCurrentState: true, +/// listener: (context, state) { +/// // this is now called immediately with BlocA's current state +/// // in addition to new emissions +/// }, +/// ) +/// ``` +/// {@endtemplate} class BlocListener, S> extends BlocListenerBase { /// {@macro bloc_listener} @@ -83,6 +108,7 @@ class BlocListener, S> Key? key, B? bloc, BlocListenerCondition? listenWhen, + bool startWithCurrentState = false, Widget? child, }) : super( key: key, @@ -90,6 +116,7 @@ class BlocListener, S> listener: listener, bloc: bloc, listenWhen: listenWhen, + startWithCurrentState: startWithCurrentState, ); } @@ -109,6 +136,7 @@ abstract class BlocListenerBase, S> this.bloc, this.child, this.listenWhen, + this.startWithCurrentState = false, }) : super(key: key, child: child); /// The widget which will be rendered as a descendant of the @@ -127,6 +155,9 @@ abstract class BlocListenerBase, S> /// {@macro bloc_listener_listen_when} final BlocListenerCondition? listenWhen; + /// {@macro bloc_listener_start_with_current_state} + final bool startWithCurrentState; + @override SingleChildState> createState() => _BlocListenerBaseState(); @@ -142,6 +173,14 @@ abstract class BlocListenerBase, S> 'listenWhen', listenWhen, ), + ) + ..add( + FlagProperty( + 'startWithCurrentState', + value: startWithCurrentState, + ifTrue: 'with current state', + ifFalse: 'without current state', + ), ); } } @@ -209,8 +248,20 @@ class _BlocListenerBaseState, S> super.dispose(); } + Stream _startWithValue( + T value, + Stream originalStream, + ) async* { + yield value; + yield* originalStream; + } + void _subscribe() { - _subscription = _bloc.stream.listen((state) { + final stream = widget.startWithCurrentState + ? _startWithValue(_bloc.state, _bloc.stream) + : _bloc.stream; + + _subscription = stream.listen((state) { if (!mounted) return; if (widget.listenWhen?.call(_previousState, state) ?? true) { widget.listener(context, state); diff --git a/packages/flutter_bloc/test/bloc_listener_test.dart b/packages/flutter_bloc/test/bloc_listener_test.dart index 663a0f9ea66..14ba6b87c66 100644 --- a/packages/flutter_bloc/test/bloc_listener_test.dart +++ b/packages/flutter_bloc/test/bloc_listener_test.dart @@ -507,6 +507,52 @@ void main() { expect(states, expectedStates); }); + testWidgets( + 'calls listener with initial state when startWithCurrentState is true', + (tester) async { + final counterCubit = CounterCubit(); + final states = []; + const expectedStates = [0]; + await tester.pumpWidget( + BlocListener( + bloc: counterCubit, + startWithCurrentState: true, + listener: (_, state) { + states.add(state); + }, + child: const SizedBox(), + ), + ); + + await tester.pump(); + + expect(states, expectedStates); + }); + + testWidgets( + 'calls listener when further states emitted when startWithCurrentState ' + 'is true', (tester) async { + final counterCubit = CounterCubit(); + final states = []; + const expectedStates = [0, 1]; + await tester.pumpWidget( + BlocListener( + bloc: counterCubit, + startWithCurrentState: true, + listener: (_, state) { + states.add(state); + }, + child: const SizedBox(), + ), + ); + + await tester.pump(); + counterCubit.increment(); + await tester.pump(); + + expect(states, expectedStates); + }); + testWidgets('overrides debugFillProperties', (tester) async { final builder = DiagnosticPropertiesBuilder(); @@ -528,6 +574,7 @@ void main() { "bloc: Instance of 'CounterCubit'", 'has listener', 'has listenWhen', + 'without current state', ], ); });