Related
I have a bloc that emits some states User states
These are my states at the moment
part of 'user_bloc.dart';
#immutable
abstract class UserState extends Equatable {}
class UserInitial extends UserState {
#override
List<Object?> get props => [];
}
class UserCreating extends UserState {
#override
List<Object?> get props => [];
}
class UserCreated extends UserState {
late final String message;
UserCreated(this.message);
#override
List<Object?> get props => [];
}
class UserError extends UserState {
late final String error;
UserError(this.error);
#override
List<Object?> get props => [error];
}
Below Is also my events for the UserBloc
part of 'user_bloc.dart';
#immutable
abstract class UserEvent extends Equatable {
#override
List<Object?> get props => [];
}
class CreateUser extends UserEvent {
final String name;
final String email;
final String password;
final String? imageUrl;
CreateUser({
required this.name,
required this.email,
required this.password,
required this.imageUrl,
});
}
And below is my main UserBloc where I am emitting states
class UserBloc extends Bloc<UserEvent, UserState> {
UserRepository userRepository;
UserBloc(this.userRepository) : super(UserInitial()) {
on<CreateUser>((event, emit) async {
emit(UserCreating());
try {
final result = await userRepository.signup(
name: event.name,
password: event.password,
email: event.email,
);
print(result);
emit(
UserCreated('User created successfully'),
);
} on DioError catch (error) {
emit(
UserError(
error.response.toString(),
),
);
} catch (error) {
emit(
UserError(
error.toString(),
),
);
}
});
}
}
I have wrapped my MaterialApp with both Multirepository provider and muiltiblocprovider where I have all my blocs being initialised. Below is the code for that.
#override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => UserRepository()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<ThemeModeCubit>(
create: (context) => ThemeModeCubit(),
),
BlocProvider<InternetCubit>(
create: (context) => InternetCubit(connectivity),
),
BlocProvider(
create: (context) => UserBloc(
RepositoryProvider.of<UserRepository>(context),
),
)
],
child: ValueListenableBuilder(...)
And finally, I am using bloc listener inside my code to listen to changes in the bloc but I don't get any response prior to the change.
final userRepo = RepositoryProvider.of<UserRepository>(context);
child: BlocListener(
bloc: UserBloc(userRepo),
listener: (ctx, state) {
print('listener called');
if (state is UserCreating) {
print('loading emited');
QuickAlert.show(
context: context,
type: QuickAlertType.loading,
title: 'Loading',
text: 'Signing up',
);
} else if (state is UserCreated) {
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: 'User created sucessfully', //state.message,
);
} else if (state is UserError) {
QuickAlert.show(
context: context,
type: QuickAlertType.success,
text: state.error,
);
}
},
child: Form(...)
This is how I am calling my event from the user
context.read<UserBloc>().add(
CreateUser(
name: name,
email: email,
password: password,
imageUrl: imageUrl,
),
);
If the widget is not updated it's because bloc considers the emitted state to be the same as the the previous one.
I see the you extend equatable.
Equatable overrides the == operator for you, and uses props to check for equality.
Both UserCreating and UserCreated return an empty list, so probably Bloc doesn't see any change.
I would personally override the == operator to aslo check for the object types.
In case you want to keep using equatable, just make sure that the returned props actually differ.
I see you had provide a UserBloc in MultiBlocProvider outside:
BlocProvider(
create: (context) => UserBloc(
RepositoryProvider.of<UserRepository>(context),
),
)
then you shouldn't pass another new UserBloc to BlocBuilder, that will make BlocBuilder listen to different UserBloc instance, change
BlocListener(
bloc: UserBloc(userRepo),
listener: (ctx, state) {
}
to
BlocListener<UserBloc, UserState>(
listener: (ctx, state) {
}
I am crated a float action button, on click a bottom sheet will popup. i am using a bloc with freezed library. when i tape to the float action bottom once the application hot restart the bottom sheet app appeared, when i click again on the float action button there is no action happening.
bloc code:
class NoteBloc extends Bloc<NoteEvent, NoteState> {
NoteBloc() : super(const _Initial());
#override
Stream<NoteState> mapEventToState(
NoteEvent event,
) async* {
if(event is AddNoteClickedEvent){
yield const AddNoteClickedState();
}
}
}
event code:
#freezed
class NoteEvent with _$NoteEvent {
const factory NoteEvent.started() = _Started;
const factory NoteEvent.addNoteClickedEvent() = AddNoteClickedEvent;
}
state code:
#freezed
class NoteState with _$NoteState {
const factory NoteState.initial() = _Initial;
const factory NoteState.addNoteClickedState() = AddNoteClickedState;
}
bottom sheet code is:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
return BlocConsumer<NoteBloc, NoteState>(
listener: (context, state) {
state.maybeMap(
orElse: () {},
addNoteClickedState: (AddNoteClickedState state) {
return _scaffoldKey.currentState!.showBottomSheet(
(context) => const AddNewNoteBottomSheet(),
);
},
);
},
builder: (context, state) {
return Scaffold(
key: _scaffoldKey,
floatingActionButton: InkWell(
onTap: () {
BlocProvider.of<NoteBloc>(context)
.add(const NoteEvent.addNoteClickedEvent());
},
child: Icon(
Icons.add,
// Icons.save,
color: const Color(whiteColor),
size: 9.h,
),
},
);
}
}
I usually write the code like this
class NoteBloc extends Bloc<NoteEvent, NoteState> {
NoteBloc() : super(const _Initial());
#override
Stream<NoteState> mapEventToState(
NoteEvent event,
) async* {
yield* event.when(
started: () async*{
yield initial(),
},
addNoteClickedState: ()async*{
yield addNoteClickedState();
},
);
}
}
and it always work
When using freezed classes, the operator== performs deep equals, so every instance of your bloc state is equal to the previous. In such a case, a new state won't be emitted. You need your new state to be different than the previous one.
I am using flutter_bloc library to create a verification code page. Here is what I tried to do.
class PhonePage extends StatelessWidget {
static Route route() {
return MaterialPageRoute<void>(builder: (_) => PhonePage());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
create: (_) =>
ValidationCubit(context.repository<AuthenticationRepository>()),
child: PhoneForm(),
),
);
}
}
class PhoneForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<ValidationCubit, ValidationState>(
listener: (context, state) {
print('Listener has been called');
if (state.status.isSubmissionFailure) {
_showVerificationError(context);
} else if (state.status.isSubmissionSuccess) {
_showVerificationSuccess(context);
}
},
builder: (context, state) {
return Container(
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_HeaderAndTitle(),
_VerificationInput(),
_VerifyButton()
],
),
),
),
);
},
);
}
void _showVerificationError(context) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(const SnackBar(content: Text('Validation error')));
}
void _showVerificationSuccess(context) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(const SnackBar(
content: Text('Validation Success'),
backgroundColor: Colors.green,
));
}
}
...
class _VerifyButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<ValidationCubit, ValidationState>(
builder: (context, state) {
return RaisedButton.icon(
color: Colors.blue,
padding: EdgeInsets.symmetric(horizontal: 38.0, vertical: 12.0),
textColor: Colors.white,
icon: state.status.isSubmissionInProgress
? Icon(FontAwesomeIcons.ellipsisH)
: Icon(null),
label: Text(state.status.isSubmissionInProgress ? '' : 'Verify',
style: TextStyle(fontSize: 16.0)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
onPressed: state.status.isValid
? () => context.bloc<ValidationCubit>().verifyCode()
: null);
});
}
}
Now the verifyCode() function is an async function defined inside ValidationCubit. It emits states with status set to loading, success and failure. However the listener doesn't pick up those changes and show the snackbars. I couldn't figure why? I am also using the Formz library as suggested in the flutter_bloc documentation. Here is the verifyCode part.
Future<void> verifyCode() async {
if (!state.status.isValidated) return;
emit(state.copyWith(status: FormzStatus.submissionInProgress));
try {
// send validation code to server here
await _authenticationRepository.loginWithEmailAndPassword(
email: 'email#email.com', password: '12');
emit(state.copyWith(status: FormzStatus.submissionSuccess));
} on Exception {
emit(state.copyWith(status: FormzStatus.submissionFailure));
}
}
The verification code model looks like this:
class ValidationState extends Equatable {
final VerificationCode verificationCode;
final FormzStatus status;
const ValidationState({this.verificationCode, this.status});
#override
List<Object> get props => [verificationCode];
ValidationState copyWith(
{VerificationCode verificationCode, FormzStatus status}) {
return ValidationState(
verificationCode: verificationCode ?? this.verificationCode,
status: status ?? this.status);
}
}
And the validation state class is:
class ValidationState extends Equatable {
final VerificationCode verificationCode;
final FormzStatus status;
const ValidationState({this.verificationCode, this.status});
#override
List<Object> get props => [verificationCode];
ValidationState copyWith(
{VerificationCode verificationCode, FormzStatus status}) {
return ValidationState(
verificationCode: verificationCode ?? this.verificationCode,
status: status ?? this.status);
}
}
I think the problem is your state class.
listener is only called once for each state change (NOT including the initial state) unlike builder in BlocBuilder and is a void function.
Every time when a new state is emitted by the Cubit it is compared with the previous one, and if they are "DIFFERENT", the listener function is triggered.
In you situation, you are using Equatable with only verificationCode as props, which means when two states are compared only the verificationCodes are tested. In this way BLoC consumer thinks that the two states are equal and do not triggers the listener function again.
If you check your verifyCode() function the only changing parameter is status.
In order to fix that add the status property to the props list in your state class.
#override
List<Object> get props => [this.verificationCode, this.status];
if you want to update same state just add one state before calling your updating state
like this
if you want to update 'Loaded' state again call 'Loading' state before that and than call 'Loaded' state so BlocListener and BlocBuilder will listen to it
Edited
I have changed using bloc to cubit for this and cubit can emmit same state continuously and bloclistener and blocbuilder can listen to it
I needed to delete a list item from the list and app should pops a pop-up before delete. Somehow you decline the delete pop up via no button or click outside of the pop-up, last state doesn't change. After that, if you want to delete same item, it wasn't trigger cause all parameters are same with the previous state and equatable says its same. To get rid of this issue, you need to define a rand function and put just before your emit state. You need to add a new parameter to your state and you need to add to the props. It works like a charm.
My state;
class SomeDeleteOnPressedState extends SomeState
with EquatableMixin {
final int index;
final List<Result> result;
final String currentLocation;
final int rand;/// this is the unique part.
SomeDeleteOnPressedState({
required this.index,
required this.result,
required this.currentLocation,
required this.rand,
});
// don't forget to add rand parameter in props. it will make the difference here.
#override
List<Object> get props => <Object>[index, result, currentLocation, rand];
}
and my bloc;
on<SomeDeleteEvent>((event,emit){
int rand =Random().nextInt(100000);
emit(
SomeDeleteOnPressedState(
currentLocation: event.currentLocation,
index: event.index,
result: event.result,
rand: rand,/// every time it will send a different rand, so this state is always will be different.
),
);
});
Hope it helps.
I develop an app using BLoC pattern.
In my app there are 2 routes, route A and B, and both of them access same data.
A problem caused when moving the routes as below.
Move to route B from route A that shows the data.
Update the data at route B.
Back to route A.
After moving back to route A, the StreamBuilder of showing the data never updates automatically.
How can I let the StreamBuilder update on resumed state?
Here are sample codes.
routeA.dart
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).pushNamed("routeB");
},
),
],
);
}
}
routeB.dart
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.update.add(null);
},
),
);
}
}
bloc.dart
class Bloc {
Stream<String> data;
Sink<void> update;
Model _model;
Bloc() {
_model = Model();
final update = PublishSubject<void>();
this.update = update;
final data = BehaviorSubject<String>(seedValue: "");
this.data = data;
update.map((event) => _model.update()).listen((event) => data.sink.add(_model.getData()));
}
}
model.dart
class Model {
static Model _model;
factory Model() { // model is singleton.
_model ??= Model._();
return _model;
}
Model._();
int _data = 0;
void update() {
_data++;
}
String getData() {
return _data.toString();
}
}
StreamBuilder updates the data whenever it gets changed not when just by calling stream
RouteA
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (ctx) {
return RouteB();
}));
},
),
],
);
}
}
Route B
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.updateData();
},
),
);
}
}
Bloc
class Bloc {
final _update = PublishSubject<String>();
Model _model = Model();
Stream<String> get data => _update.stream;
void updateData() async {
_model.update();
_update.sink.add(_model.getData());
_update.stream.listen((event) {
print(event);
});
}
dispose() {
_update.close();
}
}
final bloc = Bloc();
just follow above changes, it will do the trick for you.
I'm trying to build a login activity using Bloc, with the help of the tutorials available at https://bloclibrary.dev/. I've successfully combined the Form Validation and Login Flow into a working solution, but things took a messy turn when adding a button to toggle password visibility.
Figured I'd follow the same format that the validations and login state had (widget's onPressed triggers an event, bloc processes it and changes state to update view), but because states are mutually exclusive, toggling the password visibility causes other information (like validation errors, or the loading indicator) to disappear, because the state that they require in order to be shown is no longer the active one.
I assume one way to avoid this is to have a separate Bloc to handle just the password toggle, but I think that involves nesting a second BlocBuilder in my view, not to mention implementing another set of Bloc+Events+States, which sounds like it might make the code harder to understand/navigate as things get more complex. Is this how Bloc is meant to be used, or is there a cleaner approach that works better here to avoid this?
class LoginForm extends StatefulWidget {
#override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
_onLoginButtonPressed() {
BlocProvider.of<LoginBloc>(context).add(
LoginButtonPressed(
username: _usernameController.text,
password: _passwordController.text,
),
);
}
_onShowPasswordButtonPressed() {
BlocProvider.of<LoginBloc>(context).add(
LoginShowPasswordButtonPressed(),
);
}
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state is LoginFailure) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('${state.error}'),
backgroundColor: Colors.red,
),
);
}
},
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return Form(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Username', prefixIcon: Icon(Icons.person)),
controller: _usernameController,
autovalidate: true,
validator: (_) {
return state is LoginValidationError ? state.usernameError : null;
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
state is! DisplayPassword ? Icons.visibility : Icons.visibility_off,
color: ColorUtils.primaryColor,
),
onPressed: () {
_onShowPasswordButtonPressed();
},
),
),
controller: _passwordController,
obscureText: state is! DisplayPassword ? true : false,
autovalidate: true,
validator: (_) {
return state is LoginValidationError ? state.passwordError : null;
},
),
Container(height: 30),
ButtonTheme(
minWidth: double.infinity,
height: 50,
child: RaisedButton(
color: ColorUtils.primaryColor,
textColor: Colors.white,
onPressed: state is! LoginLoading ? _onLoginButtonPressed : null,
child: Text('LOGIN'),
),
),
Container(
child: state is LoginLoading
? CircularProgressIndicator()
: null,
),
],
),
),
);
},
),
);
}
}
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final UserRepository userRepository;
final AuthenticationBloc authenticationBloc;
bool isShowingPassword = false;
LoginBloc({
#required this.userRepository,
#required this.authenticationBloc,
}) : assert(userRepository != null),
assert(authenticationBloc != null);
LoginState get initialState => LoginInitial();
#override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginShowPasswordButtonPressed) {
isShowingPassword = !isShowingPassword;
yield isShowingPassword ? DisplayPassword() : LoginInitial();
}
if (event is LoginButtonPressed) {
if (!_isUsernameValid(event.username) || !_isPasswordValid(event.password)) {
yield LoginValidationError(
usernameError: _isUsernameValid(event.username) ? null : "(test) validation failed",
passwordError: _isPasswordValid(event.password) ? null : "(test) validation failed",
); //TODO update this so fields are validated for multiple conditions (field is required, minimum char size, etc) and the appropriate one is shown to user
}
else {
yield LoginLoading();
final response = await userRepository.authenticate(
username: event.username,
password: event.password,
);
if (response.ok != null) {
authenticationBloc.add(LoggedIn(user: response.ok));
}
else {
yield LoginFailure(error: response.error.message);
}
}
}
}
bool _isUsernameValid(String username) {
return username.length >= 4;
}
bool _isPasswordValid(String password) {
return password.length >= 4;
}
}
abstract class LoginEvent extends Equatable {
const LoginEvent();
#override
List<Object> get props => [];
}
class LoginButtonPressed extends LoginEvent {
final String username;
final String password;
const LoginButtonPressed({
#required this.username,
#required this.password,
});
#override
List<Object> get props => [username, password];
#override
String toString() =>
'LoginButtonPressed { username: $username, password: $password }';
}
class LoginShowPasswordButtonPressed extends LoginEvent {}
abstract class LoginState extends Equatable {
const LoginState();
#override
List<Object> get props => [];
}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginValidationError extends LoginState {
final String usernameError;
final String passwordError;
const LoginValidationError({#required this.usernameError, #required this.passwordError});
#override
List<Object> get props => [usernameError, passwordError];
}
class DisplayPassword extends LoginState {}
class LoginFailure extends LoginState {
final String error;
const LoginFailure({#required this.error});
#override
List<Object> get props => [error];
#override
String toString() => 'LoginFailure { error: $error }';
}
Yup, you are not supposed to have this. // class DisplayPassword extends LoginState {}
And yes, if you want to go pure BLoC it's the correct way to go imo. In this case, because the only state you want to hold is a single bool value, you can go with a simpler approach with the BLoC structure. What I mean is, you don't need to make complete set, event class, state class, bloc class but instead just the bloc class. And on top of that, you can separate the bloc folder into 2 kinds.
bloc
- full
- login_bloc.dart
- login_event.dart
- login_state.dart
- single
- password_visibility_bloc.dart
class PasswordVisibilityBloc extends Bloc<bool, bool> {
#override
bool get initialState => false;
#override
Stream<bool> mapEventToState(
bool event,
) async* {
yield !event;
}
}