Flutter Bloc conflicting states - flutter

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;
}
}

Related

Flutter/Dart - Update state from an external class

I'm totally new to Flutter/Dart, I've done all the layouts for my application, and now it's time to make my application's API calls. I'm trying to manage the forms as cleanly as possible.
I created a class that manages TextFields data (values and errors), if my API returns an error I would like the screen to update without having to call setState(() {}), is this possible?
In addition, many of my application's screens use values that the user enters in real time, if that happened I would have to call the setState(() {}) methodmany times.
Any idea how to do this with the excess calls to the setState(() {}) method?
I created a test project for demo, these are my files:
File path: /main.dart
import 'package:flutter/material.dart';
import 'login_form_data.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
setState(() {}); // Don't want to call setState
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('email'),
labelText: 'E-mail',
),
onChanged: (value) => _loginFormData.setValue('email', value),
),
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('password'),
labelText: 'Password',
),
obscureText: true,
onChanged: (value) =>
_loginFormData.setValue('password', value),
),
ElevatedButton(
onPressed: _submitLoginForm,
child: const Text('Login'),
)
],
),
),
),
);
}
}
File path: /login_form_data.dart
import 'form/form_data.dart';
import 'form/form_field.dart';
class LoginFormData extends FormData {
#override
Map<String, FormField> fields = {
'email': FormField(),
'password': FormField(),
'simple_account': FormField(
value: true,
),
};
LoginFormData();
}
File path: /form/form_data.dart
class FormData {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
File path: /form/form_field.dart
class FormField {
dynamic value;
List errors = [];
FormField({
this.value,
});
}
You are essentially looking for a State Management solution.
There are multiple solutions (you can read about them here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)
State Management allows you to declare when you want your widgets to change state instead of having to imperatively call a setState method.
Flutter recommends Provider as a beginner solution, and you can find many tutorials online.
With that being said, let me show you how to achieve this result with a very basic solution: Change Notifier
Quoting flutter documentation :
” A class that can be extended or mixed in that provides a change
notification API using VoidCallback for notifications.”
We are going to make FormData a Change notifier, and them we are going to make your app listen to changes on the instance, and rebuild itself based on them.
Step 1:
Based on the code you posted, I can tell that you will interact with LoginFormData based on the methods setValue and setError from the parent class FormData. So we are going to make FormData inherit ChangeNotifer, and make a call to notifyListeners() on these two methods.
class FormData extends ChangeNotifier {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
notifyListeners();
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
notifyListeners();
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
Now, every time you call either setValue or setError, the instance of FormData will notify the listeners.
Step2:
Now we have to setup a widget in your app to listen to these changes. Since your app is still small, it’s easy to find a place to put this listener. But as your app grows, you will see that it gets harder to do this, and that’s where packages like Provider come in handy.
We are going to wrap your Padding widget that is the first on the body of your scaffold, with a AnimatedBuilder. Despite of the misleading name, animated builder is not limited to animations. It is a widget that receives any listenable object as a parameter, and rebuilds itself every time it gets notified, passing down the updated version of the listenable.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
//setState(() {}); No longer necessary
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: AnimatedBuilder(
animation: _loginFormData,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
//... The rest of your widgets
),
),
);
}
),
);
}
}

Flutter Cubit not updating state

Cubit State class
class AuthState extends Equatable {
const AuthState({
this.makePwdVisible = true,
});
final bool makePwdVisible;
#override
List<Object> get props => [makePwdVisible];
AuthState copyWith({
bool? makePwdVisible,
}) {
return AuthState(
makePwdVisible: makePwdVisible ?? this.makePwdVisible,
);
}
}
Cubit class to emit state
class AuthenticationCubit extends Cubit<AuthState> {
void togglePwdVisibility() {
emit(
state.copyWith(
makePwdVisible: !state.makePwdVisible
)
);
}
}
Class Contain TextFormField to accept password
class PasswordInput extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<AuthenticationCubit, AuthState>(
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
return Padding(
child: TextFormField(
obscureText: state.makePwdVisible,
onPressed: () => context.read<AuthenticationCubit>().togglePwdVisibility()
) : null,
),
),
);
},
);
}
}
I am trying to update password visibility of TextFormField but when I click the eye button of TextFormField nothing happens. Cubit is not updating the state because state.makePwdVisible not get called on eye button toggle.
While when I enter password and if password is wrong state.password.error is getting called.
I don't know why Cubit is working for one variable password.error but not for makePwdVisible

Flutter Bloc, UI not updating when state is emmited

I have an app where I'm emitting budgetLoaded() in several events. I have noticed that if I don't emit a different state with my budgetLoading even the app doesn't seem to respond to the change.
import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';
part 'budget_event.dart';
part 'budget_state.dart';
class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
final BudgetRepository budgetRepository;
BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
on<AppStarted>((event, emit) async {
emit(BudgetLoading());
if (await budgetRepository.hasBudget()) {
emit(BudgetLoaded(budget: budgetRepository.budget));
} else {
emit(BudgetLoaded(budget: budgetRepository.budget));
}
});
on<Withdraw>((event, emit) {
budgetRepository.budget.withdraw(event.amount);
emit(BudgetLoaded(budget: budgetRepository.budget));
});
on<Deposit>((event, emit) {
emit(BudgetLoading());
budgetRepository.budget.deposit(event.amount);
emit(new BudgetLoaded(budget: budgetRepository.budget));
});
}
}
This is an issue with the deposit event. This code works.
import 'package:bloc/bloc.dart';
import 'package:budget_app/model/budget.dart';
import 'package:budget_app/model/budget_repository.dart';
import 'package:equatable/equatable.dart';
part 'budget_event.dart';
part 'budget_state.dart';
class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
final BudgetRepository budgetRepository;
BudgetBloc({required this.budgetRepository}) : super(BudgetInitial()) {
on<AppStarted>((event, emit) async {
emit(BudgetLoading());
if (await budgetRepository.hasBudget()) {
emit(BudgetLoaded(budget: budgetRepository.budget));
} else {
emit(BudgetLoaded(budget: budgetRepository.budget));
}
});
on<Withdraw>((event, emit) {
budgetRepository.budget.withdraw(event.amount);
emit(BudgetLoaded(budget: budgetRepository.budget));
});
on<Deposit>((event, emit) {
budgetRepository.budget.deposit(event.amount);
emit(new BudgetLoaded(budget: budgetRepository.budget));
});
}
}
This code does not work the state that is emitted is correct, I have unit tests to test this and I've done debugging.
This is the main page, the BlocProvider is in the Main file above the Material App so it should be global.
import 'package:budget_app/bloc/budget_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WalletScreen extends StatefulWidget {
const WalletScreen({Key? key}) : super(key: key);
#override
State<WalletScreen> createState() => _WalletScreenState();
}
class _WalletScreenState extends State<WalletScreen> {
#override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => onPressed(context),
child: Icon(Icons.add),
),
body: BlocBuilder<BudgetBloc, BudgetState>(
bloc: BlocProvider.of<BudgetBloc>(context),
builder: (context, state) {
if (state is BudgetLoaded) {
var budget = state.budget;
return SafeArea(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
child: Center(
child: Column(
children: [
Text(
'Current Balance:',
style: TextStyle(
color: Colors.white, fontSize: 25),
),
Text(
'${budget.numberFormatted}',
style: TextStyle(
color: Colors.green, fontSize: 50),
),
],
),
),
color: Colors.blueGrey,
),
),
),
],
),
),
);
} else {
return CircularProgressIndicator();
}
},
),
);
}
onPressed(BuildContext context) {
var budgetBloc = BlocProvider.of<BudgetBloc>(context);
budgetBloc.add(Deposit(100.00));
}
}
It's not a huge issue I just want to understand why it doesn't work.
EVENT
part of 'budget_bloc.dart';
abstract class BudgetEvent extends Equatable {
const BudgetEvent();
#override
List<Object> get props => [];
}
class AppStarted extends BudgetEvent {}
class Withdraw extends BudgetEvent {
final double amount;
const Withdraw(this.amount);
}
class Deposit extends BudgetEvent {
final double amount;
const Deposit(this.amount);
}
STATE
part of 'budget_bloc.dart';
abstract class BudgetState extends Equatable {
const BudgetState();
#override
List<Object> get props => [];
}
class BudgetInitial extends BudgetState {}
class BudgetLoading extends BudgetState {}
class BudgetLoaded extends BudgetState {
final Budget budget;
BudgetLoaded({required this.budget});
}
You can see in the source of emit that it won't publish a state if it is equal to the last one:
void emit(State state) {
try {
if (isClosed) {
throw StateError('Cannot emit new states after calling close');
}
if (state == _state && _emitted) return;
onChange(Change<State>(currentState: this.state, nextState: state));
_state = state;
_stateController.add(_state);
_emitted = true;
} catch (error, stackTrace) {
onError(error, stackTrace);
rethrow;
}
}
By default, == returns true if two objects are the same instance. This could lead to redundant state change notifications, so we usually override == and hashCode, to explicitly tell what counts as equal.
In your example Equatable overrides == and hashCode for you so you don't have to write boilerplate code. The problem is, your props list is empty, so every instance of the same class will be equal to each other. This is why you don't see a state change after the first one.
You should override the props in BudgetLoaded, so Equatable knows what should be compared:
class BudgetLoaded extends BudgetState {
final Budget budget;
BudgetLoaded({required this.budget});
#override
List<Object> get props => [budget];
}
And don't forget to update the Budget class too so that == and hashCode say they are equal, when you think they should be equal.

Problem with events in BLoC Flutter. I always call two events instead of one

I return the state with the TodoLoadedState list in each block method. But when I call the block event in onPressed, the list itself is not returned and I have to add a second call to the block method todoBloc.add(LoadTodos()); But that's not correct. Ideas need to trigger 1 event but to perform 2 actions, the second action is to update the list. Thanks in advance!
todo_bloc
class TodoBloc extends Bloc<TodoEvent, TodoState> {
final TodoRepository todoRepository;
TodoBloc(this.todoRepository) : super(TodoEmptyState()) {
on<LoadTodos>((event, emit) async {
emit(TodoLoadingState());
try {
final List<Todo> _loadedTodoList = await todoRepository.getAllTodos();
emit(TodoLoadedState(loadedUser: _loadedTodoList));
} catch (_) {
emit(TodoErrorState());
}
});
on<CreateTodos>((event, emit) async {
// Todo todo = Todo(description: event.task, isDone: false);
await todoRepository.insertTodo(event.todo);
final List<Todo> _loadedTodoList = await todoRepository.getAllTodos();
emit(TodoLoadedState(loadedUser: _loadedTodoList));
});
on<DeleteTodos>((event, emit) async {
await todoRepository.deleteTodo(event.id);
final List<Todo> _loadedTodoList = await todoRepository.getAllTodos();
emit(TodoLoadedState(loadedUser: _loadedTodoList));
});
on<UpdateTodos>((event, emit) async {
await todoRepository.updateTodo(event.todo);
final List<Todo> _loadedTodoList = await todoRepository.getAllTodos();
emit(TodoLoadedState(loadedUser: _loadedTodoList));
});
}
}
todo_list
class TodoList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final TodoBloc todoBloc = context.read<TodoBloc>();
return BlocBuilder<TodoBloc, TodoState>(builder: (context, state) {
if (state is TodoEmptyState) {
return const Center(
child: Text(
'No Todo',
style: TextStyle(fontSize: 20.0),
),
);
}
if (state is TodoLoadingState) {
return const Center(child: CircularProgressIndicator());
}
if (state is TodoLoadedState) {
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: state.loadedUser.length,
itemBuilder: (context, index) => ListTile(
title: Column(children: [
Text('${state.loadedUser[index].description}'),
Text('${state.loadedUser[index].id}'),
]),
trailing: IconButton(
onPressed: () {
todoBloc.add(DeleteTodos(id: state.loadedUser[index].id));
todoBloc.add(LoadTodos());
},
home_page
class HomePage extends StatelessWidget {
final todoRepository = TodoRepository();
#override
Widget build(BuildContext context) {
return BlocBuilder<TodoBloc, TodoState>(builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Todos'),
),
body: SingleChildScrollView(
child: Column(
children: [
TodoList(),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add, size: 32, color: Colors.white),
onPressed: () {
final TodoBloc todoBloc = context.read<TodoBloc>();
final _todoDescriptionFromController = TextEditingController();
showModalBottomSheet(
context: context,
builder: (builder) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
color: Colors.transparent,
child: Container(
todo_state
abstract class TodoState extends Equatable {
const TodoState();
#override
List<Object> get props => [];
}
class TodoLoadingState extends TodoState {}
class TodoEmptyState extends TodoState {}
class TodoLoadedState extends TodoState {
List<dynamic> loadedUser;
TodoLoadedState({required this.loadedUser});
}
class TodoErrorState extends TodoState {}
todo_event
abstract class TodoEvent extends Equatable {
const TodoEvent();
#override
List<Object> get props => [];
}
class LoadTodos extends TodoEvent {}
class CreateTodos extends TodoEvent {
final Todo todo;
const CreateTodos(this.todo);
}
class UpdateTodos extends TodoEvent {
final Todo todo;
const UpdateTodos(this.todo);
}
class DeleteTodos extends TodoEvent {
final int id;
const DeleteTodos({required this.id});
}
class QueryTodo extends TodoEvent {}
event onPressed, everywhere you have to use 2 events to load the updated list
todoBloc.add(UpdateTodos(updateTodo));
todoBloc.add(LoadTodos());
This is the culprit:
abstract class TodoState extends Equatable {
const TodoState();
#override
List<Object> get props => [];
}
You are extending Equatable in TodoState and passing an empty list to props. When other states such as TodoLoadedState extend TodoState they inherit Equatable as well and the empty props.
If you're using Equatable make sure to pass all properties to the
props getter.
This is from bloc faq. Right now all instances of your TodoLoadedState are considered equal. Doesn't matter if you have a TodoLoadedState with hundreds of loadedUser or a TodoLoadedState with none. They are both considered equal and only the first time you pass a new TodoLoadedState the BlocBuilder will update. The consequent ones have no effect since BlocBuilder thinks it is the same as previous one. The reason your LoadTodos event causes a rebuild is that first you emit TodoLoadingState() and then in case of success TodoLoadedState(loadedUser: _loadedTodoList). This alternating between two different states makes it work.
So either don't use Equatable or make sure to pass all the properties to props.
class TodoLoadedState extends TodoState {
final List<dynamic> loadedUser;
TodoLoadedState({required this.loadedUser});
#override
List<Object?> get props => [loadedUser];
}

Flutter bloc listener not listening to state changes

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.