From a0a96460b484210cc5df8d55757472fa66195083 Mon Sep 17 00:00:00 2001 From: LukasMirbt Date: Sun, 21 Jul 2024 15:49:15 +0200 Subject: [PATCH 1/5] use emit.onEach instead of handling stream subscription manually --- examples/flutter_login/lib/app.dart | 4 +- .../bloc/authentication_bloc.dart | 55 ++-- .../bloc/authentication_event.dart | 8 +- .../lib/home/view/home_page.dart | 2 +- .../authentication_bloc_test.dart | 244 +++++++++--------- 5 files changed, 155 insertions(+), 158 deletions(-) diff --git a/examples/flutter_login/lib/app.dart b/examples/flutter_login/lib/app.dart index 0be06d2d847..ad04db2df31 100644 --- a/examples/flutter_login/lib/app.dart +++ b/examples/flutter_login/lib/app.dart @@ -39,7 +39,9 @@ class _AppState extends State { create: (_) => AuthenticationBloc( authenticationRepository: _authenticationRepository, userRepository: _userRepository, - ), + )..add( + AuthenticationSubscriptionRequested(), + ), child: const AppView(), ), ); diff --git a/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart b/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart index 8a07123d61b..e37ccdbbaf1 100644 --- a/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart +++ b/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart @@ -16,45 +16,40 @@ class AuthenticationBloc }) : _authenticationRepository = authenticationRepository, _userRepository = userRepository, super(const AuthenticationState.unknown()) { - on<_AuthenticationStatusChanged>(_onAuthenticationStatusChanged); - on(_onAuthenticationLogoutRequested); - _authenticationStatusSubscription = _authenticationRepository.status.listen( - (status) => add(_AuthenticationStatusChanged(status)), - ); + on(_onSubscriptionRequested); + on(_onLogoutPressed); } final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository; - late StreamSubscription - _authenticationStatusSubscription; - - @override - Future close() { - _authenticationStatusSubscription.cancel(); - return super.close(); - } - Future _onAuthenticationStatusChanged( - _AuthenticationStatusChanged event, + Future _onSubscriptionRequested( + AuthenticationSubscriptionRequested event, Emitter emit, ) async { - switch (event.status) { - case AuthenticationStatus.unauthenticated: - return emit(const AuthenticationState.unauthenticated()); - case AuthenticationStatus.authenticated: - final user = await _tryGetUser(); - return emit( - user != null - ? AuthenticationState.authenticated(user) - : const AuthenticationState.unauthenticated(), - ); - case AuthenticationStatus.unknown: - return emit(const AuthenticationState.unknown()); - } + await emit.onEach( + _authenticationRepository.status, + onData: (status) async { + switch (status) { + case AuthenticationStatus.unauthenticated: + return emit(const AuthenticationState.unauthenticated()); + case AuthenticationStatus.authenticated: + final user = await _tryGetUser(); + return emit( + user != null + ? AuthenticationState.authenticated(user) + : const AuthenticationState.unauthenticated(), + ); + case AuthenticationStatus.unknown: + return emit(const AuthenticationState.unknown()); + } + }, + onError: addError, + ); } - void _onAuthenticationLogoutRequested( - AuthenticationLogoutRequested event, + void _onLogoutPressed( + AuthenticationLogoutPressed event, Emitter emit, ) { _authenticationRepository.logOut(); diff --git a/examples/flutter_login/lib/authentication/bloc/authentication_event.dart b/examples/flutter_login/lib/authentication/bloc/authentication_event.dart index 812831982d0..2188f8eded4 100644 --- a/examples/flutter_login/lib/authentication/bloc/authentication_event.dart +++ b/examples/flutter_login/lib/authentication/bloc/authentication_event.dart @@ -4,10 +4,6 @@ sealed class AuthenticationEvent { const AuthenticationEvent(); } -final class _AuthenticationStatusChanged extends AuthenticationEvent { - const _AuthenticationStatusChanged(this.status); +final class AuthenticationSubscriptionRequested extends AuthenticationEvent {} - final AuthenticationStatus status; -} - -final class AuthenticationLogoutRequested extends AuthenticationEvent {} +final class AuthenticationLogoutPressed extends AuthenticationEvent {} diff --git a/examples/flutter_login/lib/home/view/home_page.dart b/examples/flutter_login/lib/home/view/home_page.dart index 53e68117a47..9c24a0dac85 100644 --- a/examples/flutter_login/lib/home/view/home_page.dart +++ b/examples/flutter_login/lib/home/view/home_page.dart @@ -29,7 +29,7 @@ class HomePage extends StatelessWidget { onPressed: () { context .read() - .add(AuthenticationLogoutRequested()); + .add(AuthenticationLogoutPressed()); }, ), ], diff --git a/examples/flutter_login/test/authentication/authentication_bloc_test.dart b/examples/flutter_login/test/authentication/authentication_bloc_test.dart index 7f974f55314..dbf96692b92 100644 --- a/examples/flutter_login/test/authentication/authentication_bloc_test.dart +++ b/examples/flutter_login/test/authentication/authentication_bloc_test.dart @@ -23,6 +23,13 @@ void main() { userRepository = _MockUserRepository(); }); + AuthenticationBloc buildBloc() { + return AuthenticationBloc( + authenticationRepository: authenticationRepository, + userRepository: userRepository, + ); + } + group('AuthenticationBloc', () { test('initial state is AuthenticationState.unknown', () { final authenticationBloc = AuthenticationBloc( @@ -33,135 +40,132 @@ void main() { authenticationBloc.close(); }); - blocTest( - 'emits [unauthenticated] when status is unauthenticated', - setUp: () { - when(() => authenticationRepository.status).thenAnswer( - (_) => Stream.value(AuthenticationStatus.unauthenticated), - ); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], - ); + group('AuthenticationSubscriptionRequested', () { + final error = Exception('oops'); - blocTest( - 'emits [authenticated] when status is authenticated', - setUp: () { - when(() => authenticationRepository.status).thenAnswer( - (_) => Stream.value(AuthenticationStatus.authenticated), - ); - when(() => userRepository.getUser()).thenAnswer((_) async => user); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.authenticated(user), - ], - ); - }); + blocTest( + 'emits [unauthenticated] when status is unauthenticated', + setUp: () { + when(() => authenticationRepository.status).thenAnswer( + (_) => Stream.value(AuthenticationStatus.unauthenticated), + ); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.unauthenticated(), + ], + ); - group('AuthenticationStatusChanged', () { - blocTest( - 'emits [authenticated] when status is authenticated', - setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); - when(() => userRepository.getUser()).thenAnswer((_) async => user); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.authenticated(user), - ], - ); + blocTest( + 'emits [authenticated] when status is authenticated', + setUp: () { + when(() => authenticationRepository.status).thenAnswer( + (_) => Stream.value(AuthenticationStatus.authenticated), + ); + when(() => userRepository.getUser()).thenAnswer((_) async => user); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.authenticated(user), + ], + ); - blocTest( - 'emits [unauthenticated] when status is unauthenticated', - setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer((_) => Stream.value(AuthenticationStatus.unauthenticated)); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], - ); + blocTest( + 'emits [authenticated] when status is authenticated', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); + when(() => userRepository.getUser()).thenAnswer((_) async => user); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.authenticated(user), + ], + ); - blocTest( - 'emits [unauthenticated] when status is authenticated but getUser fails', - setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); - when(() => userRepository.getUser()).thenThrow(Exception('oops')); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], - ); + blocTest( + 'emits [unauthenticated] when status is unauthenticated', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer( + (_) => Stream.value(AuthenticationStatus.unauthenticated)); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.unauthenticated(), + ], + ); - blocTest( - 'emits [unauthenticated] when status is authenticated ' - 'but getUser returns null', - setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); - when(() => userRepository.getUser()).thenAnswer((_) async => null); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], - ); + blocTest( + 'emits [unauthenticated] when status is authenticated but getUser fails', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); + when(() => userRepository.getUser()).thenThrow(Exception('oops')); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.unauthenticated(), + ], + ); - blocTest( - 'emits [unknown] when status is unknown', - setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer((_) => Stream.value(AuthenticationStatus.unknown)); - }, - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - expect: () => const [ - AuthenticationState.unknown(), - ], - ); + blocTest( + 'emits [unauthenticated] when status is authenticated ' + 'but getUser returns null', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); + when(() => userRepository.getUser()).thenAnswer((_) async => null); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.unauthenticated(), + ], + ); + + blocTest( + 'emits [unknown] when status is unknown', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer((_) => Stream.value(AuthenticationStatus.unknown)); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + expect: () => const [ + AuthenticationState.unknown(), + ], + ); + + blocTest( + 'adds error when status stream emits an error', + setUp: () { + when( + () => authenticationRepository.status, + ).thenAnswer((_) => Stream.error(error)); + }, + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), + errors: () => [error], + ); + }); }); - group('AuthenticationLogoutRequested', () { + group('AuthenticationLogoutPressed', () { blocTest( - 'calls logOut on authenticationRepository ' - 'when AuthenticationLogoutRequested is added', - build: () => AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ), - act: (bloc) => bloc.add(AuthenticationLogoutRequested()), + 'calls logOut on authenticationRepository ', + build: buildBloc, + act: (bloc) => bloc.add(AuthenticationLogoutPressed()), verify: (_) { verify(() => authenticationRepository.logOut()).called(1); }, From 47b221f3d88db1580d490475dae47043657fca58 Mon Sep 17 00:00:00 2001 From: LukasMirbt Date: Fri, 26 Jul 2024 13:58:14 +0200 Subject: [PATCH 2/5] fix typo --- docs/src/content/docs/tutorials/flutter-login.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/tutorials/flutter-login.mdx b/docs/src/content/docs/tutorials/flutter-login.mdx index 4d83cf86fd7..84631e789c1 100644 --- a/docs/src/content/docs/tutorials/flutter-login.mdx +++ b/docs/src/content/docs/tutorials/flutter-login.mdx @@ -237,7 +237,7 @@ In the `_onSubscriptionRequested` event handler, the `AuthenticationBloc` uses ` `emit.onEach` creates a stream subscription internally and takes care of canceling it when either `AuthenticationBloc` or the `status` stream is closed. -If the `status` stream emits an error, `addError` forwards the error and stackTrace to any `BlocObserver´ listening. +If the `status` stream emits an error, `addError` forwards the error and stackTrace to any `BlocObserver` listening. :::caution If `onError` is omitted, any errors on the `status` stream are considered unhandled, and will be thrown by `onEach`. As a result, the subscription to the `status` stream will be canceled. From 04d76c25f3f8201ae660790c32a39be7c7301e15 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Sun, 28 Jul 2024 15:06:38 -0500 Subject: [PATCH 3/5] chore: minor adjustments --- .../bloc/authentication_bloc.dart | 4 +- .../authentication_bloc_test.dart | 37 +++++-------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart b/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart index e37ccdbbaf1..b006141ea96 100644 --- a/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart +++ b/examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart @@ -26,8 +26,8 @@ class AuthenticationBloc Future _onSubscriptionRequested( AuthenticationSubscriptionRequested event, Emitter emit, - ) async { - await emit.onEach( + ) { + return emit.onEach( _authenticationRepository.status, onData: (status) async { switch (status) { diff --git a/examples/flutter_login/test/authentication/authentication_bloc_test.dart b/examples/flutter_login/test/authentication/authentication_bloc_test.dart index fa131b607ed..c381403d289 100644 --- a/examples/flutter_login/test/authentication/authentication_bloc_test.dart +++ b/examples/flutter_login/test/authentication/authentication_bloc_test.dart @@ -32,10 +32,7 @@ void main() { group('AuthenticationBloc', () { test('initial state is AuthenticationState.unknown', () { - final authenticationBloc = AuthenticationBloc( - authenticationRepository: authenticationRepository, - userRepository: userRepository, - ); + final authenticationBloc = buildBloc(); expect(authenticationBloc.state, const AuthenticationState.unknown()); authenticationBloc.close(); }); @@ -52,9 +49,7 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], + expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( @@ -67,9 +62,7 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.authenticated(user), - ], + expect: () => const [AuthenticationState.authenticated(user)], ); blocTest( @@ -82,25 +75,19 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.authenticated(user), - ], + expect: () => const [AuthenticationState.authenticated(user)], ); blocTest( 'emits [unauthenticated] when status is unauthenticated', setUp: () { - when( - () => authenticationRepository.status, - ).thenAnswer( + when(() => authenticationRepository.status).thenAnswer( (_) => Stream.value(AuthenticationStatus.unauthenticated), ); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], + expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( @@ -114,9 +101,7 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], + expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( @@ -130,9 +115,7 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.unauthenticated(), - ], + expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( @@ -144,9 +127,7 @@ void main() { }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), - expect: () => const [ - AuthenticationState.unknown(), - ], + expect: () => const [AuthenticationState.unknown()], ); blocTest( From 3462c1446ad9be92becc7542038c90da6e4168a8 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Sun, 28 Jul 2024 15:10:05 -0500 Subject: [PATCH 4/5] chore: minor updates to docs --- docs/src/content/docs/tutorials/flutter-login.mdx | 4 ++-- examples/flutter_login/lib/app.dart | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/tutorials/flutter-login.mdx b/docs/src/content/docs/tutorials/flutter-login.mdx index 84631e789c1..0b4a0d1c85c 100644 --- a/docs/src/content/docs/tutorials/flutter-login.mdx +++ b/docs/src/content/docs/tutorials/flutter-login.mdx @@ -244,7 +244,7 @@ If `onError` is omitted, any errors on the `status` stream are considered unhand ::: :::tip -A [`BlocObserver`](https://bloclibrary.dev/bloc-concepts/#blocobserver-1) is ideal for logging Bloc events, errors and changes and can be useful for analytics and crash reporting.`; +A [`BlocObserver`](/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors and state changes especially in the context analytics and crash reporting.; ::: When the `status` stream emits `AuthenticationStatus.unknown` or `unauthenticated`, the corresponding `AuthenticationState` is emitted. @@ -281,7 +281,7 @@ We are injecting a single instance of the `AuthenticationRepository` and `UserRe `RepositoryProvider` is used to provide the single instance of `AuthenticationRepository` to the entire application which will come in handy later on. ::: -By default, `BlocProvider` is lazy and does not call `create` until the first time the Bloc is accessed. Since `AuthenticationBloc` should always subscribe to the `AuthenticationStatus` stream immediately (by adding the `AuthenticationSubscriptionRequested` event), we can explicitly opt out of this behavior by setting `lazy: false`. +By default, `BlocProvider` is lazy and does not call `create` until the first time the Bloc is accessed. Since `AuthenticationBloc` should always subscribe to the `AuthenticationStatus` stream immediately (via the `AuthenticationSubscriptionRequested` event), we can explicitly opt out of this behavior by setting `lazy: false`. `AppView` is a `StatefulWidget` because it maintains a `GlobalKey` which is used to access the `NavigatorState`. By default, `AppView` will render the `SplashPage` (which we will see later) and it uses `BlocListener` to navigate to different pages based on changes in the `AuthenticationState`. diff --git a/examples/flutter_login/lib/app.dart b/examples/flutter_login/lib/app.dart index 3db34f0571a..754f98faebd 100644 --- a/examples/flutter_login/lib/app.dart +++ b/examples/flutter_login/lib/app.dart @@ -40,9 +40,7 @@ class _AppState extends State { create: (_) => AuthenticationBloc( authenticationRepository: _authenticationRepository, userRepository: _userRepository, - )..add( - AuthenticationSubscriptionRequested(), - ), + )..add(AuthenticationSubscriptionRequested()), child: const AppView(), ), ); From 2fe759a622a58b32c30ff7c402b7c48e350770ff Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Sun, 28 Jul 2024 15:13:22 -0500 Subject: [PATCH 5/5] chore: minor docs adjustment --- docs/src/content/docs/tutorials/flutter-login.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/tutorials/flutter-login.mdx b/docs/src/content/docs/tutorials/flutter-login.mdx index 0b4a0d1c85c..8e763b4a8f4 100644 --- a/docs/src/content/docs/tutorials/flutter-login.mdx +++ b/docs/src/content/docs/tutorials/flutter-login.mdx @@ -244,7 +244,7 @@ If `onError` is omitted, any errors on the `status` stream are considered unhand ::: :::tip -A [`BlocObserver`](/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors and state changes especially in the context analytics and crash reporting.; +A [`BlocObserver`](/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors, and state changes especially in the context analytics and crash reporting.; ::: When the `status` stream emits `AuthenticationStatus.unknown` or `unauthenticated`, the corresponding `AuthenticationState` is emitted.