I have a class like below where I am tagging people to events.
class Event extends StatefulWidget {
const Event({super.key, required this.profile});
final Profile profile;
@override
State<Event> createState() {
return _EventState();
}
}
class _EventState extends State<Event> {
final List<Event> _registeredEvent = List.from([]);
void _openAddEventOverlay() {
showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
context: context,
builder: (ctx) => NewEvent(onAddEvent: _addEvent, profile: widget.profile,),
);
}
// SOME METHOD TO ADD AN EVENT
// SOME METHOD TO REMOVE AN EVENT
// SOME METHOD TO UPDATE AN EVENT
@override
Widget build(BuildContext context) {
if (_registeredEvent.isNotEmpty) {
mainContent = EventList(events: _registeredEvent, onRemoveEvent: _removeEvent,);
}
return Scaffold(
appBar: AppBar(
title: const Text('Event Tracker', style: TextStyle(
fontFamily: 'sfpro',
),),
leading: IconButton( // Add this for the left-side button
onPressed: _openAddEventOverlay,
icon: const Icon(Icons.menu), // Replace with your desired icon
),
actions: [
IconButton(
onPressed: _openAddEventOverlay,
icon: const Icon(Icons.add)
)],),
body: Column(
children: [
Chart(events: _registeredEvent),
Expanded(
child: mainContent
)],),
);
}
}
In the next screen, I’m tagging attendents for RSVP. If I don’t, considering it as a personal event. When I click on the PLUS icon in appbar, I get below:
class NewEvent extends StatefulWidget {
const NewEvent({super.key, required this.onAddEvent, required this.profile});
final void Function(Event event) onAddEvent;
final Profile profile;
@override
State<StatefulWidget> createState() {
return _NewEventState();
}
}
class _NewEventState extends State<NewEvent> {
final _eventTitleController = TextEditingController();
final _eventAmountController = TextEditingController();
final _eventPaidByController = TextEditingController();
DateTime? _eventDate;
late EventType _eventType = EventType.self;
EventCategory _eventCategory = EventCategory.sports;
int? _selectedSegment = 0;
void _presentDatePicker() async {
final now = DateTime.now();
final firstDate = DateTime(now.year-100, now.month, now.day);
final pickedDate = await showDatePicker(context: context, initialDate: now, firstDate: firstDate, lastDate: now);
setState(() {
_eventDate = pickedDate;
});
}
void _openAddPayersOverlay() {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => CreateGroupEvent(eventTitle: _eventTitleController.text.toString(), eventAmount: _eventAmountController.text.toString(), profile: widget.profile, eventCategory: _eventCategory, eventDate: _eventDate ?? DateTime.now(), onAddGroupEvent: widget.onAddEvent,),
)
);
}
void _showDialog() {
if (Platform.isAndroid) {
showDialog(context: context, builder: (ctx) =>
AlertDialog(
title: const Text('Invalid Input'),
content: const Text('Please make sure all mandatory fields are entered'),
actions: [
TextButton(onPressed: () {
Navigator.pop(ctx);
},
child: const Text('Okay'),),
],
),);
} else {
showCupertinoDialog(context: context, builder: (ctx) =>
CupertinoAlertDialog(
title: const Text('Invalid Input'),
content: const Text('Please make sure all mandatory fields are entered'),
actions: [
TextButton(onPressed: () {
Navigator.pop(ctx);
},
child: const Text('Okay'),),
],
),);
}
}
void _submitEventData() {
final enteredAmount = double.tryParse(_eventAmountController.text);
final invalidAmount = enteredAmount == null || enteredAmount <= 0;
if (_eventTitleController.text.trim().isEmpty || _eventPaidByController.text.trim().isEmpty || invalidAmount || _eventDate == null) {
_showDialog();
return;
}
widget.onAddEvent(
Event(eventTitle: _eventTitleController.text,
eventAmount: enteredAmount,
eventDate: _eventDate!,
eventCreator: "${widget.profile.firstName} ${widget.profile.lastName}",
eventType: _eventType,
eventCategory: _eventCategory,
eventPaidBy: _eventPaidByController.text,
profileId: widget.profile.profileId
),
);
Navigator.pop(context);
}
@override
void dispose() {
_eventTitleController.dispose();
_eventAmountController.dispose();
_eventPaidByController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;
final dateFormatter = DateFormat.yMd();
return LayoutBuilder(builder: (ctx, constraints) {
return SizedBox(
height: double.infinity,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.fromLTRB(16, 65, 16, keyboardSpace+16),
child: Column(children: [
SOME TEXTFIELD1,
SOME TEXTFIELD2,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('* Ask for RSVP of the wedding here.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
background: Paint()
..color = CupertinoColors.placeholderText.darkHighContrastElevatedColor
..strokeWidth = 20
..strokeJoin = StrokeJoin.round
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
color: Colors.black54,
)),
const SizedBox(width: 5,),
GestureDetector(
onTap: () {
_openAddPayersOverlay();
},
child: const Text("Click here to add attendents", style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)),
),
],
),
const SizedBox(height: 100,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligns children with space between
children: [
CupertinoButton(
onPressed: () {
Navigator.pop(context);
},
color: CupertinoColors.systemRed,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: const Text('Cancel Event'),
),
Expanded( // Pushes the Save Event button to the right
child: Container(),
),
CupertinoButton(
onPressed: () {
_submitEventData();
},
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: const Text('Review'),
),
],
)
],
),
),
),
);
});
}
}
This is how a card looks in my home page.
If I click on the text: "Click here to add attendents"
in the overlay I go to a search field and select people from the search results.
Class handling addition of people:
class CreateGroupEvent extends StatefulWidget {
const CreateGroupEvent({
super.key,
required this.eventTitle,
required this.eventAmount,
required this.eventDate,
required this.eventCategory,
required this.profile,
required this.onAddGroupEvent,
});
final Profile profile;
final String eventTitle;
final String eventAmount;
final EventCategory eventCategory;
final DateTime eventDate;
final Function(Event) onAddGroupEvent;
@override
State<StatefulWidget> createState() {
return _CreateGroupEvent();
}
}
class _CreateGroupEvent extends State<CreateGroupEvent> {
List<Map<String, dynamic>> _taggedUsers = [];
List<Map<String, dynamic>> addedAttendents = [];
void _runFilter(String enteredKeyWord) {
List<Map<String, dynamic>> results = [];
if(enteredKeyWord.isEmpty) {
results = widget.profile.getProfileAttendents();
} else {
results = widget.profile.getProfileAttendents().where((user) =>
user["firstname"].toString().toLowerCase().contains(enteredKeyWord.toString().toLowerCase())
||
user["lastname"].toString().toLowerCase().contains(enteredKeyWord.toString().toLowerCase())
).toList();
}
setState(() {
_taggedUsers = results;
});
}
void _addFriendToEvent(Map<String, dynamic> attendent) {
if (addedAttendents.any((addedFriend) => addedFriend['id'] == attendent['id'])) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${attendent["firstname"]} ${attendent["lastname"]} is already added!'),
duration: const Duration(seconds: 2), // Adjust duration as needed
),);
} else {
setState(() {
addedAttendents.add(attendent);
_taggedUsers.removeWhere((user) => user['id'] == attendent['id']);
});
}
}
void _updateGroupEvent({required Profile profile, required List<Map<String, dynamic>> addAttendents, required BuildContext context}) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => UpdateGroupEvent(
profile: widget.profile,
addAttendents: addedAttendents,
eventTitle: widget.eventTitle,
eventAmount: widget.eventAmount,
eventDate: widget.eventDate,
eventCategory: widget.eventCategory,
onAddGroupEvent: widget.onAddGroupEvent
)));
}
@override
void initState() {
_taggedUsers = widget.profile.getProfileAttendents();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.eventTitle),),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
const SizedBox(height: 20,),
TextField(
onChanged: (value) => _runFilter(value),
decoration: const InputDecoration(labelText: "Search for an attendent"),
),
const SizedBox(height: 20,),
Expanded(
child: ListView.builder(
itemCount: _taggedUsers.length,
itemBuilder: (context, index) => Row(
children: [
Expanded(child: Card(
key: ValueKey(_taggedUsers[index]["id"]),
color: Colors.white70, elevation: 0.2,
shape: RoundedRectangleBorder(side: const BorderSide(color: Colors.black, width: 1), borderRadius: BorderRadius.circular(8)),
margin: const EdgeInsets.symmetric(vertical: 10),
child: ListTile(
title: Text(_taggedUsers[index]["firstname"].toString().toUpperCase(), style: const TextStyle(fontSize: 16, color: Colors.black)),
subtitle: Text(_taggedUsers[index]["lastname"].toString().toUpperCase(), style: const TextStyle(fontSize: 14, color: Colors.black)),
trailing: GestureDetector(
onTap: () { _addFriendToEvent(_taggedUsers[index]);},
child: const Text('ADD', style: TextStyle(fontSize: 10, color: Colors.black))
),),),),],)),),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligns children with space between
children: [
CupertinoButton(
onPressed: () {
Navigator.pop(context);
},
color: CupertinoColors.systemRed,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: const Text('Cancel Event'),
),
Expanded( // Pushes the Save Event button to the right
child: Container(),
),
CupertinoButton(
onPressed: () {
Navigator.of(context, rootNavigator: false).pop();
_updateGroupEvent(context: context, profile: widget.profile, addAttendents: addedAttendents);
},
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: const Text('Next'),
),],)],),),);
}
}
And I am applying some custom logic in the next page, have two buttons "Cancel event" and "Create event". Upon clicking the "Create event" button, the app should take me back to the Events screen(second screenshot) with a new card, with the title from the variable: eventTitle
right below the wedding card.
Below is the class I wrote:
class UpdateGroupEvent extends StatefulWidget {
const UpdateGroupEvent({
super.key,
required this.profile,
required this.addAttendents,
required this.eventTitle,
required this.eventAmount,
required this.eventDate,
required this.eventCategory,
required this.onAddGroupEvent,
});
final Profile profile;
final String eventTitle;
final String eventAmount;
final EventCategory eventCategory;
final DateTime eventDate;
final List<Map<String, dynamic>> addAttendents;
final Function(Event) onAddGroupEvent; // Callback to add group event
@override
State<StatefulWidget> createState() {
return _UpdateGroupEvent();
}
}
class _UpdateGroupEvent extends State<UpdateGroupEvent> {
int? _eventSplitSegment = 1;
late bool _splitEqually = false;
final _totalAmountController = TextEditingController();
final _receivedEventAmountController = TextEditingController();
List<TextEditingController> _amountControllers = [];
@override
void initState() {
super.initState();
// Initialize amount controllers for each attendent
_amountControllers = List.generate(
widget.addAttendents.length,
(index) => TextEditingController(),
);
// Initialize _receivedEventAmountController with eventAmount
_receivedEventAmountController.text = widget.eventAmount;
}
@override
void dispose() {
super.dispose();
_totalAmountController.dispose();
_receivedEventAmountController.dispose();
for (var controller in _amountControllers) {
controller.dispose();
}
}
double parseToDouble(String value) {
double? parsedValue = double.tryParse(value);
if (parsedValue == null) {
return 0.00;
}
return parsedValue;
}
void _submitEventData({required double totalGroupEventAmount}) {
// 1. Calculate total entered amount
double totalEnteredAmount = 0;
for (var controller in _amountControllers) {
final amountText = controller.text;
if (amountText.isNotEmpty) {
totalEnteredAmount += double.tryParse(amountText) ?? 0;
}
}
// 2. Get total event amount
final totalEventAmount =
double.tryParse(_totalAmountController.text) ?? 0;
// 3. Compare and show result
if (totalEnteredAmount == totalGroupEventAmount) {
final newEvent = Event(
eventTitle: widget.eventTitle,
eventAmount: totalGroupEventAmount,
eventDate: widget.eventDate,
eventCreator: "${widget.profile.firstName} ${widget.profile.lastName}",
eventType: EventType.group,
eventCategory: widget.eventCategory,
eventPaidBy: "${widget.profile.firstName} ${widget.profile.lastName}",
profileId: widget.profile.profileId,
);
// Add the event using the callback
widget.onAddGroupEvent(newEvent);
// Navigate back to the events page with an optional result
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => Events(profile: widget.profile), // Pass the profile data if needed
),
);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => Events(profile: widget.profile)),
(route) => false,
);
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: const Text(
'Total entered amount does not match the total event amount.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.eventTitle, style: const TextStyle(color: Colors.white, fontSize: 20),)
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Row(
children: [
// SOME OTHER TEXTFIELDS
// SOME FIELDS WITH LOGIC
// Add cards here
Expanded(
child: ListView.builder(
itemCount: widget.addAttendents.length,
itemBuilder: (context, index) {
final user = widget.addAttendents[index];
return Dismissible( // Wrap the Card with Dismissible
key: Key(user['id'].toString()), // Unique key for each Dismissible
direction: DismissDirection.startToEnd, // Swipe direction
onDismissed: (direction) {
final removedAttendent = widget.addAttendents[index];
final removedAmountController = _amountControllers[index];
setState(() {
widget.addAttendents.removeAt(index);
_amountControllers.removeAt(index);
if (_splitEqually && widget.addAttendents.isNotEmpty) {
final totalAmount = parseToDouble(widget.eventAmount);
final amountPerAttendent = ((totalAmount / widget.addAttendents.length) * 100.00).ceil() / 100.00;
for (var controller in _amountControllers) {
controller.text = amountPerAttendent.toStringAsFixed(2);
}
}
});
// Show Snackbar with undo action
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${removedAttendent['firstname']} ${removedAttendent['lastname']} deleted'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
setState(() {
// Insert the removed attendent and amount controller back into their original positions
widget.addAttendents.insert(index, removedAttendent);
_amountControllers.insert(index, removedAmountController);
// Recalculate and update split amounts if necessary
if (_splitEqually) {
final totalAmount = parseToDouble(widget.eventAmount);
final amountPerAttendent = ((totalAmount / widget.addAttendents.length) * 100.00).ceil() / 100.00;
for (var controller in _amountControllers) {
controller.text = amountPerAttendent.toStringAsFixed(2);
}
}
});
},
),
),
);
},
background: Container(
color: Colors.red,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 16.0),
child: const Icon(Icons.delete, color: Colors.white),
),
child: Card(
margin: const EdgeInsets.all(10),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${user['firstname']} ${user['lastname']}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
],
),
),
SizedBox(
width: 100,
child: TextField(
controller: _amountControllers[index],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
),
),
],
),
),
),
);
},
),
),
const SizedBox(height: 10,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
onPressed: () {
Navigator.pop(context);
},
color: CupertinoColors.systemRed,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: const Text('Cancel Event'),
),
Expanded(
child: Container(),
),
CupertinoButton(
onPressed: () {
double groupEventAmountDouble = parseToDouble(widget.eventAmount);
if (!_splitEqually) {
_submitEventData(totalGroupEventAmount: groupEventAmountDouble);
} else {
final newEvent = Event(
eventTitle: widget.eventTitle,
eventAmount: groupEventAmountDouble,
eventDate: widget.eventDate,
eventCreator: "${widget.profile.firstName} ${widget.profile.lastName}",
eventType: EventType.group,
eventCategory: widget.eventCategory,
eventPaidBy: "${widget.profile.firstName} ${widget.profile.lastName}",
profileId: widget.profile.profileId,
);
widget.onAddGroupEvent(newEvent);
// Navigate back to the events page with an optional result
// Navigator.of(context).pushReplacement(
// MaterialPageRoute(
// builder: (context) => Events(profile: widget.profile), // Pass the profile data if needed
// ),
// );
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => Events(profile: widget.profile)),
(route) => false,
);
}
},
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: const Text('Create event'),
),
],
)
],
),
),
);
}
}
When I click on "Create Event", it is taking me to the Events page however, it is erasing all the previous content on that page.
In the below screenshot, you can see the existin WEDDING
is erased now.
I tried multiple ways to go back to my Events screen.
Navigator.of(context).popUntil((route) => route.settings.name == '/events');
-> Gave me a black screen.Navigator.of(context, rootNavigator: true).pop();
-> Takes me back to the screen:CreateGroupEvent
with search field. Creates the event but I need to manually click on CANCEL backwards up till the Event(first) page.
I understand the question is so big but I want to provide as much info as possible.
Could anyone let me know if I am doing any mistake here? Any suggestions would be a massive help.
2
Answers
use
Navigator.of(context).popUntil
to navigate back and avoid overwriting the Events screen.In your
CreateGroupEvent
orUpdateGroupEvent
screen, when the event is created:_
I hope that helps.
If I understood correctly, I had the same problem, I used a Transparent Route and it worked.
A transparent Route, even in full screen, keeps the state of the Parent Route.