Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(flutter_login): use emit.onEach in AuthenticationBloc #4211

Merged
merged 6 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions docs/src/content/docs/tutorials/flutter-login.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ In the following tutorial, we're going to build a Login Flow in Flutter using th
## Key Topics

- [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children.
- [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states.
- Using Cubit instead of Bloc. [What's the difference?](/bloc-concepts#cubit-vs-bloc)
- Adding events with [context.read](/flutter-bloc-concepts#contextread).
- Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable).
Expand Down Expand Up @@ -192,8 +191,8 @@ Use the [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=F

In this application, the `AuthenticationBloc` will be reacting to two different events:

- `AuthenticationStatusChanged`: notifies the bloc of a change to the user's `AuthenticationStatus`
- `AuthenticationLogoutRequested`: notifies the bloc of a logout request
- `AuthenticationSubscriptionRequested`: initial event that notifies the bloc to subscribe to the `AuthenticationStatus` stream
- `AuthenticationLogoutPressed`: notifies the bloc of a user logout action

<RemoteCode
url="https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_login/lib/authentication/bloc/authentication_event.dart"
Expand Down Expand Up @@ -232,15 +231,25 @@ The `AuthenticationBloc` manages the authentication state of the application whi

The `AuthenticationBloc` has a dependency on both the `AuthenticationRepository` and `UserRepository` and defines the initial state as `AuthenticationState.unknown()`.

In the constructor body, the `AuthenticationBloc` subscribes to the `status` stream of the `AuthenticationRepository` and adds an `AuthenticationStatusChanged` event internally in response to a new `AuthenticationStatus`.
In the constructor body, `AuthenticationEvent` subclasses are mapped to their corresponding event handlers.

In the `_onSubscriptionRequested` event handler, the `AuthenticationBloc` uses `emit.onEach` to subscribe to the `status` stream of the `AuthenticationRepository` and emit a state in response to each `AuthenticationStatus`.

`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.

:::caution
The `AuthenticationBloc` overrides `close` in order to dispose both the `StreamSubscription` as well as the `AuthenticationRepository`.
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.
:::

Next, the `EventHandler` handles transforming the incoming `AuthenticationEvent` instances into new `AuthenticationState` instances.
:::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.;
:::

When the `status` stream emits `AuthenticationStatus.unknown` or `unauthenticated`, the corresponding `AuthenticationState` is emitted.

When an `AuthenticationStatusChanged` event is added if the associated status is `AuthenticationStatus.authenticated`, the `AuthentictionBloc` queries the user via the `UserRepository`.
When `AuthenticationStatus.authenticated` is emitted, the `AuthentictionBloc` queries the user via the `UserRepository`.

## main.dart

Expand Down Expand Up @@ -272,6 +281,8 @@ 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 (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`.

## Splash
Expand Down
3 changes: 2 additions & 1 deletion examples/flutter_login/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ class _AppState extends State<App> {
return RepositoryProvider.value(
value: _authenticationRepository,
child: BlocProvider(
lazy: false,
create: (_) => AuthenticationBloc(
authenticationRepository: _authenticationRepository,
userRepository: _userRepository,
),
)..add(AuthenticationSubscriptionRequested()),
child: const AppView(),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,40 @@ class AuthenticationBloc
}) : _authenticationRepository = authenticationRepository,
_userRepository = userRepository,
super(const AuthenticationState.unknown()) {
on<_AuthenticationStatusChanged>(_onAuthenticationStatusChanged);
on<AuthenticationLogoutRequested>(_onAuthenticationLogoutRequested);
_authenticationStatusSubscription = _authenticationRepository.status.listen(
(status) => add(_AuthenticationStatusChanged(status)),
);
on<AuthenticationSubscriptionRequested>(_onSubscriptionRequested);
on<AuthenticationLogoutPressed>(_onLogoutPressed);
}

final AuthenticationRepository _authenticationRepository;
final UserRepository _userRepository;
late StreamSubscription<AuthenticationStatus>
_authenticationStatusSubscription;

@override
Future<void> close() {
_authenticationStatusSubscription.cancel();
return super.close();
}

Future<void> _onAuthenticationStatusChanged(
_AuthenticationStatusChanged event,
Future<void> _onSubscriptionRequested(
AuthenticationSubscriptionRequested event,
Emitter<AuthenticationState> 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());
}
) {
return 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<AuthenticationState> emit,
) {
_authenticationRepository.logOut();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
2 changes: 1 addition & 1 deletion examples/flutter_login/lib/home/view/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class _LogoutButton extends StatelessWidget {
return ElevatedButton(
child: const Text('Logout'),
onPressed: () {
context.read<AuthenticationBloc>().add(AuthenticationLogoutRequested());
context.read<AuthenticationBloc>().add(AuthenticationLogoutPressed());
},
);
}
Expand Down
Loading