-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
How to handle multiple success states with rxdart? #4342
Comments
https://bloclibrary.dev/tutorials/flutter-login/#app Looking at this example, this seems impossible to do with rxdart and streams. |
Here is a condensed reproducible example: ChangeEmailView.dart class ChangeEmailView extends StatefulWidget {
const ChangeEmailView({super.key});
@override
State<ChangeEmailView> createState() => _ChangeEmailViewState();
}
class _ChangeEmailViewState extends State<ChangeEmailView> {
final GlobalKey<FormBuilderState> formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext parentContext) {
return Scaffold(
body: BlocProvider(
create: (context) => ChangeEmailBloc(
authenticationRepo: context.read<AuthenticationRepo>(),
)..add(const ChangeEmailEvent.streamRequested()),
child: BlocListener<ChangeEmailBloc, ChangeEmailState>(
listener: (BuildContext context, ChangeEmailState state) async {
// I want to navigate users to either BottomNavView() or SignUpView()
// If the user taps "I want to keep using" -> ChangeEmailEvent.emailNotChanged()
// We should navigate to BottomNavView()
// If the user taps "Change email" -> ChangeEmailEvent.emailChanged(email: email)
// We should navigate to SignUpView()
},
child: Column(
children: [
FormBuilderTextField(name: "email"),
Column(
children: [
BlocBuilder<ChangeEmailBloc, ChangeEmailState>(
builder: (context, state) {
final ErrorModel? error = state.error;
final bool isErrorVisible = state.status == ChangeEmailStatus.failure &&
error != null &&
!error.isRetryable;
if (isErrorVisible) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
error.message,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
);
} else {
return const SizedBox.shrink();
}
},
),
BlocBuilder<ChangeEmailBloc, ChangeEmailState>(
builder: (context, state) {
return IntroBottomButton(
"Change email",
isLoading: state.status == ChangeEmailStatus.loading,
isEnabled: state.status != ChangeEmailStatus.initial,
onPressed: () async {
final FormBuilderState? currentState = formKey.currentState;
final bool? isFormValid = currentState?.saveAndValidate();
if (currentState == null || isFormValid != true) {
return;
}
final String email = formKey.currentState?.value["email"];
final ChangeEmailEvent newEmailLinkEvent = ChangeEmailEvent.emailChanged(
email: email,
);
context.read<ChangeEmailBloc>().add(newEmailLinkEvent);
return;
},
);
},
),
BlocBuilder<ChangeEmailBloc, ChangeEmailState>(
builder: (context, state) {
final bool isEnabled = [ChangeEmailStatus.initial, ChangeEmailStatus.loading]
.contains(state.status);
return TextButton(
onPressed: isEnabled
? () {
const ChangeEmailEvent emailNotChangedEvent =
ChangeEmailEvent.emailNotChanged();
context.read<ChangeEmailBloc>().add(emailNotChangedEvent);
}
: null,
child: Text(
"I want to keep using\n${state.email}",
textAlign: TextAlign.center,
),
);
},
),
],
),
],
),
),
),
);
}
} ChangeEmailState part of 'change_email_bloc.dart';
enum ChangeEmailStatus { initial, loading, success, failure }
@freezed
class ChangeEmailState with _$ChangeEmailState {
const factory ChangeEmailState({
required ChangeEmailStatus status,
required String? email,
required ErrorModel? error,
}) = _ChangeEmailState;
} ChangeEmailBloc class ChangeEmailBloc extends Bloc<ChangeEmailEvent, ChangeEmailState> {
ChangeEmailBloc({required AuthenticationRepo authenticationRepo})
: _authenticationRepo = authenticationRepo,
super(const ChangeEmailState(
status: ChangeEmailStatus.initial,
email: null,
error: null,
)) {
on<_StreamRequested>(_onStreamRequested);
on<_EmailChanged>(_onEmailChanged);
on<_EmailNotChanged>(_onEmailNotChanged);
}
final AuthenticationRepo _authenticationRepo;
Future<void> _onStreamRequested(_StreamRequested event, Emitter<ChangeEmailState> emit) async {
emit(
state.copyWith(status: ChangeEmailStatus.initial, error: null),
);
await emit.forEach<AuthenticationModel>(
_authenticationRepo.getAuthentication(),
onData: (AuthenticationModel authentication) {
return state.copyWith(
status: ChangeEmailStatus.success,
email: authentication.email,
error: null,
);
},
onError: (Object error, StackTrace stackTrace) {
final AuthenticationModel? lastValue = _authenticationRepo.getLastValue();
return state.copyWith(
status: ChangeEmailStatus.failure,
email: lastValue?.email,
error: error as ErrorModel,
);
},
);
}
Future<void> _onEmailChanged(_EmailChanged event, Emitter<ChangeEmailState> emit) async {
emit(
state.copyWith(status: ChangeEmailStatus.loading, error: null),
);
await _authenticationRepo.changeEmail(event.email);
}
Future<void> _onEmailNotChanged(_EmailNotChanged event, Emitter<ChangeEmailState> emit) async {
emit(
state.copyWith(status: ChangeEmailStatus.loading, error: null),
);
await _authenticationRepo.skipChangeEmail();
}
}
ChangeEmailEvent part of 'change_email_bloc.dart';
@freezed
sealed class ChangeEmailEvent with _$ChangeEmailEvent {
const factory ChangeEmailEvent.streamRequested() = _StreamRequested;
const factory ChangeEmailEvent.emailChanged({
required String email,
}) = _EmailChanged;
const factory ChangeEmailEvent.emailNotChanged() = _EmailNotChanged;
} AuthenticationRepo class AuthenticationRepo {
AuthenticationRepo() {
unawaited(_init());
}
final SupabaseClient _supabase = Supabase.instance.client;
final _authenticationStreamController = BehaviorSubject<AuthenticationModel>();
Stream<AuthenticationModel> getAuthentication() =>
_authenticationStreamController.asBroadcastStream();
AuthenticationModel? getLastValue() => _authenticationStreamController.valueOrNull;
Future<void> _init() async {
try {
final UserResponse userFromDb = await _supabase.auth.getUser();
final AuthenticationModel authenticationModel = AuthenticationModel(
email: userFromDb.user?.email,
);
_authenticationStreamController.add(authenticationModel);
return;
} catch (e, s) {
const ErrorModel error = ErrorModel(
title: "Error",
message: "Unable to grab current email. Please try again.",
isRetryable: true,
);
_authenticationStreamController.addError(error);
return;
}
}
Future<void> changeEmail(String email) async {
final UserAttributes userAttributes = UserAttributes(
email: email,
);
try {
final UserResponse res = await _supabase.auth.updateUser(
userAttributes,
emailRedirectTo: 'https://datefirefly.com/account-verified',
);
final AuthenticationModel authenticationModel =
_authenticationStreamController.value.copyWith(
email: res.user?.newEmail,
);
_authenticationStreamController.add(authenticationModel);
return;
} catch (e, s) {
final ErrorModel publicError = buildUserFacingError(e);
_authenticationStreamController.addError(publicError);
return;
}
}
Future<void> skipChangeEmail(String email) async {
try {
final res = await _supabase.rpc(
'skip-email',
);
final AuthenticationModel authenticationModel =
_authenticationStreamController.value.copyWith(
email: res.user?.newEmail,
);
_authenticationStreamController.add(authenticationModel);
return;
} catch (e, s) {
final ErrorModel publicError = buildUserFacingError(e);
_authenticationStreamController.addError(publicError);
return;
}
}
} You can see that whether or not the the event _onEmailChanged or _onEmailNotChanged is used, the resulting status is |
You are in full control of the state so there's nothing stopping you from changing the state to contain another property/enum: enum ChangeEmailStatus { initial, loading, success, failure }
@freezed
class ChangeEmailState with _$ChangeEmailState {
const factory ChangeEmailState({
required ChangeEmailStatus status,
required bool emailChanged,
required String? email,
required ErrorModel? error,
}) = _ChangeEmailState;
} Then you can conditionally do whatever you need to do based on the In general, if the user decides not to change their email why do you even need to notify the bloc at all? There doesn't appear to be any logic needed -- you can just imperatively navigate to whatever screen/page you want directly. |
I'm following the todos guide and changed it to my use case.
I'm having trouble understanding how to actually handle different success/loading states in the UI.
Here is what I have so far.
My bloc file.
The _StreamRequested event is called upon creation of the blocprovider.
This sets the state to initial while I do all the fetching in the repo and then the repo adds a value to the stream and a the status is then changed to
success
and on success, the UI is updated to display the new value.The save button in my UI calls the
_onEmailChanged
event.This changes the state to loading and my UI then updates the save button to say loading.
Inside the repo, if this call is successful
_authenticationRepo.changeEmail
it adds the value to stream, which then triggers this bloc'sonData
and the status is set to success. Otherwise,_authenticationRepo.changeEmail
adds an error and theonError
is called and the status is set to failure.This issue I'm having is that in the UI, I want to create a listener that displays a popup if
_authenticationRepo.changeEmail
is successful.But as you can see, there's currently two instances of when success would happen and potentially even more in the future.
I can't make my UI listener just listen to when the status is success, because that'll even happen during initialization.
So what's the correct/proper way to handle this?
Thanks!
The text was updated successfully, but these errors were encountered: