I have a program with three screen.
Navigation
- HomeScreen using bottomnav and children to show a list of OrderListScreen, HistoryScreen.
- In the OrderListScreen, when the user click on the create order button it will jump to the CreateOrderScreen
Background
- Using Flutter Bloc
- When the form submission success or failure I will set the state to either sucess or failure and use BlocListener to show the corresponding ScaffoldMessenger in Snackbar in CreateOrderScreen based on the state.
- When the user click on the submit button and then click the back button, since I am submitting using bloc state, it will have chance the order submit success but the user doesn’t know bcoz he get out of the CreateOrderScreen back to HomeScreen before the ScaffoleMessager is shosing the message in Snackbar so that I also listen to the same bloc state in both HomeScreen and CreateOrderScreen. To avoid showing the message twice, I have issue a event to reset the state to initialstate.
Test
To test the snackbar, I make some mistake on form submission in order to make sure I got the error message.
Problem
- When I submit a form with failure, I got the snackbar show in the CreateScreen which is good
- Then I wait for the message to disappear
- Then I click on the back button and also check the console to ensure
the state is OrderFormInitial, suppose I won’t see the snackbar in
the HomeScreen after getting back? But the reality is I see the snackbar appear on the HomeScreen as well.
Debug Message
flutter: OrderFormBloc - OrderFormSubmitOrderEvent: Error submitting order: type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>'
flutter: HomeScreen - _showMessage: Order submission failed: type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>'
flutter: CreateOrderScreen - _showMessage: Order submission failed: type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>'
flutter: OrderFormBloc - Constructor : state: Instance of 'OrderFormFailure'
flutter: OrderFormBloc - OrderFormResetEvent: Resetting form fields
flutter: HomeScreen - build: state is OrderFormInitial
flutter: CreateOrderScreen - build: state is OrderFormInitial
flutter: OrderFormBloc - Constructor : state: Instance of 'OrderFormInitial'
flutter: OrderFormBloc - OrderFormResetEvent: Resetting form fields
flutter: HomeScreen - build: state is OrderFormInitial
flutter: CreateOrderScreen - build: state is OrderFormInitial
flutter: OrderFormBloc - Constructor : state: Instance of 'OrderFormInitial'
Assumption
In the code, you will see lots of multibloc provider bcoz I am going to extend the program to listen on multiple bloc and show corresponding message.
Extracted Code
Here is the extracted code, I just extract the relevant code to keep the post short
HomeScreen
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> {
int _selectedTabIndex = 0;
final List<Widget> _children = [
const OrderListScreen(),
const HistoryScreen(),
];
void onTabTapped(int index) {
debugPrint('Tab $index tapped');
setState(() {
_selectedTabIndex = index;
});
}
Future<void> _showMessage(BuildContext context, String message) async {
debugPrint('HomeScreen - _showMessage: $message');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
Future<void> _vibrate() async {
if (await Vibration.hasVibrator() ?? false) {
debugPrint('HomeScreen - _vibrate: Vibrate');
Vibration.vibrate(duration: 500); // Vibrate for 500ms
}
}
@override
Widget build(BuildContext context) {
debugPrint('Building HomeScreen');
final orderFormBloc =
BlocProvider.of<OrderFormBloc>(context);
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(
title: const Text('App'),
leading: Builder(
builder: (context) => PlatformIconButton(
icon: const Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
),
body: MultiBlocListener(
listeners: [
BlocListener<OrderFormBloc, OrderFormState>(
listener: (context, state) {
if (state is OrderFormSuccess) {
_showMessage(
context, 'Congratulations, order submitted!');
orderFormBloc
.add(OrderFormResetEvent()); // Reset state
} else if (state is OrderFormFailure) {
_vibrate();
_showMessage(context,
'Order submission failed: ${state.error}');
orderFormBloc
.add(OrderFormResetEvent()); // Reset state
} else if (state is OrderFormInitial) {
debugPrint(
'HomeScreen - build: state is OrderFormInitial');
}
},
),
// Add other BlocListeners here as needed
],
child: _children[_selectedTabIndex],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedTabIndex,
onTap: onTabTapped,
items: [
BottomNavigationBarItem(icon: ordersIcon(), label: "Orders"),
BottomNavigationBarItem(icon: historyIcon(), label: "History"),
],
),
drawer: const CbAppDrawer(),
),
);
}
}
CreateOrderScreen
class CreateOrderScreen extends StatefulWidget {
const CreateOrderScreen({super.key});
@override
CreateOrderScreenState createState() => CreateOrderScreenState();
}
class CreateOrderScreenState extends State<CreateOrderScreen> {
Future<void> _showMessage(BuildContext context, String message) async {
debugPrint('CreateOrderScreen - _showMessage: $message');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
Future<void> _vibrate() async {
if (await Vibration.hasVibrator() ?? false) {
debugPrint('CreateOrderScreen - _vibrate: Vibrate');
Vibration.vibrate(duration: 500); // Vibrate for 500ms
}
}
@override
Widget build(BuildContext context) {
final orderFormBloc =
BlocProvider.of<OrderFormBloc>(context);
debugPrint('CreateOrderScreen: build');
return DefaultTabController(
length: 2,
child: ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(
title: PlatformText('Create Order'),
bottom: const TabBar(
tabs: [
Tab(text: 'Standard Order'),
Tab(text: 'VIP Order'),
],
),
),
body: MultiBlocListener(
listeners: [
BlocListener<OrderFormBloc, OrderFormState>(
listener: (context, state) {
if (state is OrderFormSuccess) {
_showMessage(context,
'Congratulations, order submitted!');
orderFormBloc
.add(OrderFormResetEvent()); // Reset state
} else if (state is OrderFormFailure) {
_vibrate();
_showMessage(context,
'Order submission failed: ${state.error}');
orderFormBloc
.add(OrderFormResetEvent()); // Reset State
} else if (state is OrderFormInitial) {
debugPrint(
'CreateOrderScreen - build: state is OrderFormInitial');
}
},
),
],
child: const TabBarView(
children: [
StandardOrder(),
VIPOrder(),
],
),
),
),
),
);
}
}
OrderFormEvent
part of '../order_form_bloc.dart';
// Define Events
abstract class OrderFormEvent {}
class OrderFormClearEvent extends OrderFormEvent {}
class OrderFormResetEvent extends OrderFormEvent {}
class OrderFormSubmitOrderEvent extends OrderFormEvent {
final Order order;
final String token;
OrderFormSubmitOrderEvent(this.order, this.token);
}
FormState
part of '../order_form_bloc.dart';
abstract class OrderFormState {}
class OrderFormInitial extends OrderFormState {}
class OrderFormSubmitting extends OrderFormState {}
class OrderFormSuccess extends OrderFormState {}
class OrderFormFailure extends OrderFormState {
final String error;
OrderFormFailure(this.error);
}
FormBloc
part 'states/order_form_state.dart';
part 'events/order_form_event.dart';
class OrderFormBloc
extends Bloc<OrderFormEvent, OrderFormState>
with UserValidatorMixin {
final ConfigCubit configCubit;
final BehaviorSubject<String> _contactPerson =
BehaviorSubject<String>.seeded('');
// Sink getters
Function(String) get changeContactPerson => _contactPerson.sink.add;
// Stream getters for validation
late Stream<String> contactPersonStream;
OrderFormBloc({required this.configCubit})
: super(OrderFormInitial()) {
_initializeItems();
on<OrderFormSubmitOrderEvent>((event, emit) async {
emit(OrderFormSubmitting());
try {
debugPrint('Submitting order with type of service: $event');
String order =
await OrderRepository.createOrder(event.token, event.order);
debugPrint('Order successfully created with ID: $order');
emit(OrderFormSuccess());
debugPrint(
'OrderFormBloc - OrderFormSubmitOrderEvent: Adding Clear form fields event');
add(OrderFormClearEvent());
} catch (e) {
emit(OrderFormFailure(e.toString()));
}
});
on<OrderFormClearEvent>((event, emit) {
debugPrint(
'OrderFormBloc - OrderFormClearEvent: Clearing form fields');
_clearFormFields();
});
on<OrderFormResetEvent>((event, emit) {
debugPrint(
'OrderFormBloc - OrderFormResetEvent: Resetting form fields');
emit(OrderFormInitial());
});
}
void _initializeItems() {
contactPersonStream = _contactPerson.stream.transform(
StreamTransformer.fromBind((stream) => validateName(stream)));
}
void submitOrder(String token, OrderTypeEnum orderType) async {
final Order order = Order(
contactPerson: _contactPerson.value,
);
String orderId = await OrderRepository.createOrder(token, order);
debugPrint('Order successfully created with ID: $orderId');
} catch (error) {
debugPrint('Failed to create order: $error');
}
}
void _clearFormFields() {
_contactPerson.sink.add('');
}
@override
Future<void> close() {
_contactPerson.close();
return super.close();
}
}