fix: Bloc Consumer's listener is passing the previous instance of the bloc to the method
yarenalbayr opened this issue · comments
Issue: State Reversion after Navigation in Flutter Bloc
Description
I have a Flutter app where the UserEditProfileBloc
is used in the "Edit Profile Details" screen. The bloc is created using a FutureBuilder
that fetches the user's profile details. When navigating to a separate screen using BlocConsumer
's listener and passing the bloc instance through the Navigator
to edit certain details, the state is updated correctly. However, after popping back to the "Edit Profile Details" screen and navigating again, the previous state is passed.
Reproduction Steps
-
Initial Setup:
- The
UserEditProfileBloc
is initialized usingBlocProvider
within aFutureBuilder
. - This Future fetches user profile details, passing the data into the bloc's constructor.
- The
-
Navigating to the Editing Screen:
- The user navigates to a specific editing screen by triggering a navigation event.
- The
BlocConsumer
's listener is used to handle navigation events.
-
Editing and Returning:
- After performing edits and popping back to the "Edit Profile Details" screen, the updated data is correctly reflected.
-
Navigating Again:
- When navigating to the same editing screen again, the previous state (from before the first edit) is shown instead of the updated state.
Bloc Setup
The UserEditProfileBloc
handles two types of events:
- Profile Update Events: These events handle updating profile details, such as interests, in the database.
- Navigation Events: These events handle navigation actions.
Code
The issue might relate to how the bloc state is handled across navigation. Below are snippets of the bloc, the screen setup, and navigation:
1. Bloc Setup
part 'user_edit_profile_state.dart';
class UserEditProfileBloc
extends Bloc<UserEditProfileEventBase, UserEditProfileState> {
final UsersRepository userRepository;
final UserProfileDetailsDM userProfileDetails;
UserEditProfileBloc(
{required this.userRepository, required this.userProfileDetails})
: super(UserEditProfileInitial(userProfileDetails)) {
on<UserEditProfileEvent>(_onUserEditProfileEvent);
on<NavigationActionEvent>(_onUserEditProfileNavigationEvent);
}
Future<void> _onUserEditProfileEvent(
UserEditProfileEvent event, Emitter<UserEditProfileState> emit) async {
emit(UserEditProfileLoading(this.userProfileDetails));
try {
final result = await event.map(
updateInterests: (e) => userRepository.updateInterests(e.interests),
//other methods to call repository
);
result.fold(
(userProfileDetails) =>
emit(UserEditProfileSuccess(userProfileDetails)),
(exception) => emit(UserEditProfileFailureActionState(
this.userProfileDetails, exception.toString())),
);
} catch (exception) {
emit(UserEditProfileFailureActionState(
this.userProfileDetails, exception.toString()));
}
}
FutureOr<void> _onUserEditProfileNavigationEvent(
NavigationActionEvent event, Emitter<UserEditProfileState> emit) {
emit(UserEditProfileLoading(this.userProfileDetails));
emit(UserEditProfileNavigationActionState(
this.userProfileDetails,
event.destination,
));
}
}
Using freezed package for bloc's events
part 'user_edit_profile_event.freezed.dart';
abstract class UserEditProfileEventBase {}
abstract class UserEditProfileNavigationEventBase
extends UserEditProfileEventBase {}
@freezed
sealed class UserEditProfileEvent extends UserEditProfileEventBase
with _$UserEditProfileEvent {
const factory UserEditProfileEvent.updateInterests(
List<String> interests) = UpdateInterests;
// Other events
}
@freezed
sealed class UserEditProfileNavigationEvent
extends UserEditProfileNavigationEventBase
with _$UserEditProfileNavigationEvent {
const factory UserEditProfileNavigationEvent.navigationActionEvent(
UserEditProfileActionType destination, // this is just an enum for the navigation action types
) = NavigationActionEvent;
}
My bloc
part 'user_edit_profile_state.dart';
class UserEditProfileBloc
extends Bloc<UserEditProfileEventBase, UserEditProfileState> {
final UsersRepository userRepository;
final UserProfileDetailsDM userProfileDetails;
UserEditProfileBloc(
{required this.userRepository, required this.userProfileDetails})
: super(UserEditProfileInitial(userProfileDetails)) {
on<UserEditProfileEvent>(_onUserEditProfileEvent);
on<NavigationActionEvent>(_onUserEditProfileNavigationEvent);
}
Future<void> _onUserEditProfileEvent(
UserEditProfileEvent event, Emitter<UserEditProfileState> emit) async {
emit(UserEditProfileLoading(this.userProfileDetails));
try {
final result = await event.map(
updateInterests: (e) => userRepository.updateInterests(e.interests),
//other methods to call repository
);
result.fold(
(userProfileDetails) =>
emit(UserEditProfileSuccess(userProfileDetails)),
(exception) => emit(UserEditProfileFailureActionState(
this.userProfileDetails, exception.toString())),
);
} catch (exception) {
emit(UserEditProfileFailureActionState(
this.userProfileDetails, exception.toString()));
}
}
FutureOr<void> _onUserEditProfileNavigationEvent(
NavigationActionEvent event, Emitter<UserEditProfileState> emit) {
emit(UserEditProfileLoading(this.userProfileDetails));
emit(UserEditProfileNavigationActionState(
this.userProfileDetails,
event.destination,
));
}
Edit Profile Details View
FutureBuilder<Result<UserProfileDetailsDM, Exception>>(
future: container.resolve<UsersRepository>().getProfileDetails(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(l10n.editProfileDetailsErrorFetchingProfile));
} else {
return BlocProvider<UserEditProfileBloc>(
create: (context) => UserEditProfileBloc(
userRepository: container.resolve<UsersRepository>(),
userProfileDetails: snapshot.data!.getOrThrow()),
child:
BlocConsumer<UserEditProfileBloc, UserEditProfileState>(
listener: (context, state) {
if (state is UserEditProfileFailureActionState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.editProfileDetailsSomethingWentWrong),
backgroundColor: Colors.red,
),
);
}
//Handling navigation via listener in here
if (state is UserEditProfileNavigationActionState) {
handleNavigationAction(
context,
state.destination,
context.read<UserEditProfileBloc>(),
state.profile);
}
},
buildWhen: (previous, current) =>
current is UserEditProfileSuccess,
listenWhen: (previous, current) =>
current is UserEditProfileActionState,
builder: (context, state) {
final bloc = context.watch<UserEditProfileBloc>();
final userProfileDetails = state.profile;
return Stack(
children: [
SingleChildScrollView(
child: Container(
margin: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyInterests(
selectedInterests:
userProfileDetails.interests?.toList(),
userEditProfileBloc: bloc,
),
My interest is showing a chip of widget in edit profile and it will call the navigation event on tap
In MyInterest widget there is
onTap: () {
widget.userEditProfileBloc.add(
UserEditProfileNavigationEvent
.navigationActionEvent(
UserEditProfileActionType.interests,
));
}),
The event that is in my
mixin EditProfileDetailsScreenMixin on State<EditProfileDetailsScreen
Future<void> handleNavigationAction(
BuildContext context,
UserEditProfileActionType actionType,
UserEditProfileBloc bloc) async {
switch (actionType) {
case UserEditProfileActionType.interests:
pushToInterests(context, bloc);
case UserEditProfileActionType.location:
//some other implementations
Future<dynamic> pushToInterests(
BuildContext context, UserEditProfileBloc bloc) {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: bloc,
child: SelectInterests(
userEditProfileBloc: bloc,
),
),
));
}
The SelectInterests page shows the selected interests of that user and allows user to select more or less and on pop it calls
void saveAndSubmit() {
widget.userEditProfileBloc
.add(UserEditProfileEvent.updateInterests(userInterests.value));
Navigator.of(context).pop();
}
```
Hi @yarenalbayr 👋
Thanks for opening an issue!
Are you able to provide a link to a minimal reproduction sample? It'd be much easier to help if I'm able to run your sample and reproduce/debug the issue locally.
Closing for now since there's no link to a minimal reproduction sample. If this is still an issue please share a link to a minimal reproduction sample and I'm more than happy to take a closer look, thanks!