skip to Main Content

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'),
                  ),
                ],
              )
            ],
            ),
          ),
        ),
      );
    });
  }
}

enter image description here

This is how a card looks in my home page.
enter image description here

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.
enter image description here

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'),
                ),
              ],
            )
          ],
        ),
      ),
    );
  }
}

enter image description here

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.

enter image description here

I tried multiple ways to go back to my Events screen.

  1. Navigator.of(context).popUntil((route) => route.settings.name == '/events'); -> Gave me a black screen.
  2. 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


    • Preserve State

    use Navigator.of(context).popUntil to navigate back and avoid overwriting the Events screen.

    • Pass Back Data

    In your CreateGroupEvent or UpdateGroupEvent screen, when the event is created:

    Navigator.of(context).pop(newEvent);
    
    • Handle Returned Data

    _

    void _openAddEventOverlay() async {
      final newEvent = await Navigator.push(
        context,
        MaterialPageRoute(
          builder: (ctx) => NewEvent(onAddEvent: _addEvent, profile: widget.profile),
        ),
      );
    
      if (newEvent != null) {
        setState(() {
          _registeredEvent.add(newEvent);
        });
      }
    }
    

    I hope that helps.

    Login or Signup to reply.
  1. If I understood correctly, I had the same problem, I used a Transparent Route and it worked.

    class TransparentPageRoute extends PageRoute with MaterialRouteTransitionMixin {
      @override
      final bool maintainState;
      final WidgetBuilder builder;
    
      TransparentPageRoute(
          {required this.builder, super.settings, super.fullscreenDialog = false, this.maintainState = false});
    
      @override
      bool get opaque => false;
    
      @override
      Widget buildContent(BuildContext context) {
        return builder(context);
      }
    }
    

    A transparent Route, even in full screen, keeps the state of the Parent Route.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search