Exception with PageController in Flutter: "A PagingController was used after being disposed"

110 views Asked by At

Hello Flutter Community,

I'm encountering an exception in my Flutter application that seems related to a PagingController being used after disposal. However, in my implementation, I am using a PageController and not a PagingController. The exception occurs during a scheduler callback and the stack trace points to the PagingController.

Here's the exception I'm facing:

════════ Exception caught by scheduler library ═════════════════════════════════
The following _Exception was thrown during a scheduler callback:
Exception: A PagingController was used after being disposed.
Once you have called dispose() on a PagingController, it can no longer be used.
If you’re using a Future, it probably completed after the disposal of the owning widget.
Make sure dispose() has not been called yet before using the PagingController.

When the exception was thrown, this was the stack:
#0      PagingController._debugAssertNotDisposed.<anonymous closure> (package:infinite_scroll_pagination/src/core/paging_controller.dart:133:9)
paging_controller.dart:133
#1      PagingController._debugAssertNotDisposed (package:infinite_scroll_pagination/src/core/paging_controller.dart:142:6)
paging_controller.dart:142
#2      PagingController.notifyPageRequestListeners (package:infinite_scroll_pagination/src/core/paging_controller.dart:203:12)
paging_controller.dart:203
#3      _PagedSliverBuilderState._buildListItemWidget.<anonymous closure> (package:infinite_scroll_pagination/src/ui/paged_sliver_builder.dart:255:29)
paged_sliver_builder.dart:255
#4      SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1325:15)
binding.dart:1325
#5      SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1264:9)
binding.dart:1264
#6      SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1113:5)
binding.dart:1113
#7      _invoke (dart:ui/hooks.dart:312:13)
hooks.dart:312
#8      PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:383:5)
platform_dispatcher.dart:383
#9      _drawFrame (dart:ui/hooks.dart:283:31)
hooks.dart:283
════════════════════════════════════════════════════════════════════════════════

In my application, I make sure to dispose of the PageController correctly in the dispose method of my widget. Here's a snippet of my code related to the PageController:

import 'package:bework/src/components/app_bar_widget.dart';
import 'package:bework/src/screens/dashboard_screen.dart';
import 'package:bework/src/screens/project.dart';
import 'package:bework/src/screens/records_screen.dart';
import 'package:bework/src/screens/time_off_screen.dart';
import 'package:flutter/material.dart';
import 'package:nb_utils/nb_utils.dart';

class MWBottomNavigationScreen2 extends StatefulWidget {
  @override
  MWBottomNavigationScreen2State createState() => MWBottomNavigationScreen2State();
}

class MWBottomNavigationScreen2State extends State<MWBottomNavigationScreen2> {
  int isSelected = 0;
  bool pageControllerIsDisposed = false;
  late PageController pageController;

  @override
  void initState() {
    super.initState();
    print('Navigation Screen 1');
    pageController = PageController();
    print('Navigation Screen 2');
  }

  @override
  void setState(fn) {
    if (mounted) super.setState(fn);
  }

  @override
  void dispose() {
    print('Navigation Screen 3');
    pageControllerIsDisposed = true;
    pageController.dispose();
    print('Navigation Screen 4');
    super.dispose();
  }

  Widget tabItem(var pos, var icon) {
    return GestureDetector(
      onTap: () async {
        try {
          print('Navigation Screen 5');
        if (pageControllerIsDisposed) return;
        if (!mounted) return;

        setState(() {
          isSelected = pos;
           
        });
        await pageController.animateToPage(
              isSelected,
              duration: Duration(milliseconds: 300),
              curve: Curves.easeInOut,
            );
          print('Navigation Screen 6');
        } catch (e) {
            print('ERROR ANIMATING SCREEN');

            print(e);
          }
      },
      child: Padding(
        padding: EdgeInsets.all(0.0),
        child: Container(
          alignment: Alignment.center,
          decoration: isSelected == pos ? BoxDecoration(shape: BoxShape.rectangle, color: Color(0xffe4aa4c)) : BoxDecoration(),
          child: Image.asset(
            icon,
            width: 30,
            height: 30,
            color: isSelected == pos ? black : white,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CustomAppBar(),
      body: PageView(
        controller: pageController,
        onPageChanged: (index) {
          if (pageControllerIsDisposed) return;
          setState(() {
            isSelected = index;
          });
        },
        children: [
          Dashboard(name: 'John Doe'),
          ProjectTasks(),
          TimeOffScreen(),
          RecordsScreen(),
        ],
      ),
      bottomNavigationBar: Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          Container(
            height: 60,
            decoration: BoxDecoration(
              color: black,
              boxShadow: [
                BoxShadow(
                  color: shadowColorGlobal,
                  blurRadius: 10,
                  spreadRadius: 2,
                ),
              ],
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Flexible(
                  child: tabItem(0, 'assets/images/icons/home.png'),
                  flex: 1,
                ),
                Flexible(
                  child: tabItem(1, 'assets/images/icons/ballot-check.png'),
                  flex: 1,
                ),
                Flexible(
                  child: tabItem(2, 'assets/images/icons/plane-departure.png'),
                  flex: 1,
                ),
                Flexible(
                  child: tabItem(3, 'assets/images/icons/search-alt.png'),
                  flex: 1,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Here is the code where I use paged_vertical_calendar, a dependent of infinite_scroll_pagination

import 'package:bework/src/classes/allocations.dart';
import 'package:bework/src/classes/time_off.dart';
import 'package:bework/src/classes/time_off_type.dart';
import 'package:bework/src/components/create_edit_timeoff.dart';
import 'package:bework/src/components/filter_dialog.dart';
import 'package:bework/src/services/odoo_communication_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:nb_utils/nb_utils.dart';
import 'package:paged_vertical_calendar/paged_vertical_calendar.dart';
import 'package:paged_vertical_calendar/utils/date_utils.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class TimeOffScreen extends StatefulWidget {
  @override
  _TimeOffScreenState createState() => _TimeOffScreenState();
}

class _TimeOffScreenState extends State<TimeOffScreen> {
  late OdooCommunicationService ocs;
  late List<TimeOffType> timeOffList;
  late List<Allocations> allocationsList;
  String content = "";
  bool isMounted = false; // Add this variable
  var filters = null;


  @override
  void initState() {
    super.initState();
    isMounted = true;
    ocs = Get.find<OdooCommunicationService>();
    timeOffList = [];
    allocationsList = [];
    getTimeOffList();
  }

  void getTimeOffList([List<String>? selectedStates]) async {
    try {
      EasyLoading.show(
        status: 'loading...',
        maskType: EasyLoadingMaskType.black,
      );
      // Ir buscar a lista de states selecionados
      // Passar neste método a lista de states com o bool a true
      // Passar apenas a chave do state, ex: 'draft', 'paid', 'approved', 'refused'
      print('GET TIME OFF LIST');
      print(selectedStates);
      selectedStates ??= <String>[];
      List<TimeOffType> result = await ocs.getListTimeOff(selectedStates);
      List<Allocations> result1 = await ocs.getDaysAllRequest();

      if (isMounted) {
        // Check if the widget is still mounted before calling setState
        setState(() {
          if (result.isNotEmpty) {
            timeOffList = result;
          }
          if (result1.isNotEmpty) {
            allocationsList = result1;
            for (var i = 0; i < allocationsList.length; i++) {
              // String content = "Are you sure you want to create this time off request?\n\n" +
              //                 "Time Off Type: " + selectedTimeOffType.name + "\n" +
              //                 "Start Date: " + selectedTimeOffType.date_from.toString().split(' ')[0] + "\n" +
              //                 "End Date: " + selectedTimeOffType.date_to.toString().split(' ')[0] + "\n" +
              //                 "Duration: " + number_of_days_display.toString() + " " + duration['value']['number_of_hours_text'] + "\n";

              content +=  "\n" + allocationsList[i].name + "\n" +
                          allocationsList[i].usable_remaining_leaves + " " +
                          allocationsList[i].request_unit.capitalizeFirstLetter() + 's Available' + "\n" +
                          "Valid Until " + allocationsList[i].expire_date +  "\n";
            }
          } else {
            content = "No time off type available";
          }
        });
      }
      EasyLoading.dismiss();
    }on Exception catch (e) {
      print('ERROR TIME OFF LIST');
      e.printError();
      print(e.obs);
      EasyLoading.dismiss();
    }
  }

  @override
  void dispose() {
    isMounted = false; // Set isMounted to false when the widget is disposed
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: white,
      body: Column(
        children: [
          Container(
            decoration: BoxDecoration(
              color: Color.fromARGB(255, 237, 237, 237),
            ),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Padding(padding: EdgeInsets.only(left: 16)),
                Stack(
                  alignment: Alignment.topRight,
                  children: [
                    Container(
                      width: 40,
                      height: 40,
                      margin: EdgeInsets.only(bottom: 8),
                      decoration: BoxDecoration(
                        color: Color(0xffe4aa4c),
                      ),
                      child: IconButton(
                        icon: Image.asset('assets/images/icons/filter.png', height: 25, width: 25, color: black),
                        onPressed: () {
                          showGeneralDialog(
                            context: context,
                            barrierColor: Colors.black.withOpacity(0.5),
                            barrierDismissible: true,
                            barrierLabel: '',
                            transitionDuration: Duration(milliseconds: 500),
                            pageBuilder: (context, animation1, animation2) {
                              return Center(
                                child: FilterDialog(wantStatus: true, statusList: ['confirm#' + AppLocalizations.of(context)!.toApprove, "refuse#" + AppLocalizations.of(context)!.refused, "validate1#" + AppLocalizations.of(context)!.secondApproval, "validate#" + AppLocalizations.of(context)!.approved], activeFilters: filters),
                              );
                            },
                            transitionBuilder: (context, animation1, animation2, child) {
                              const begin = Offset(0.0, -1.0);
                              const end = Offset.zero;
                              const curve = Curves.easeInOut;
                              var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                              var offsetAnimation = animation1.drive(tween);
                              return SlideTransition(position: offsetAnimation, child: child);
                            },
                          ).then((value) async {
                            print('AQUI');
                            print(value);
                            var temp = 0;
                            if (value == null) return;
                            List<String> selectedStates = [];
                            (value as Map<String, dynamic>)['status'].forEach((key, value) {
                              print(key);
                              if (value) {
                                selectedStates.add(key.split('#')[0]);
                                temp = 1;
                              }
                            });
                            getTimeOffList(selectedStates);
                            setState(() {
                              filters = value;
                              if(temp == 0){
                                filters = null;
                              }
                            });
                          });
                        },
                      ),
                    ),
                    if (filters != null) 
                      Positioned(
                        right: 5,
                        top: 5,
                        child: Container(
                          width: 10,
                          height: 10,
                          decoration: BoxDecoration(
                            color: Colors.black ,
                            shape: BoxShape.circle,
                          ),
                        ),
                      ),
                  ]
                ),
                Padding(padding: EdgeInsets.only(right: 16)),
                Container(
                  width: 40,
                  height: 40,
                  margin: EdgeInsets.only(bottom: 8),
                  decoration: BoxDecoration(
                    color: white,
                  ),
                  child: IconButton(
                    icon: Image.asset('assets/images/icons/info.png', height: 25, width: 25, color: black),
                    onPressed: () {
                      showGeneralDialog(
                        context: context,
                        barrierColor: Colors.black.withOpacity(0.5),
                        barrierDismissible: true,
                        barrierLabel: '',
                        transitionDuration: Duration(milliseconds: 500),
                        pageBuilder: (context, animation1, animation2) {
                          return Center(
                            child: AlertDialog(
                              scrollable: true,
                              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
                              backgroundColor: Color.fromARGB(255, 237, 237, 237),
                              title: Text('Informations', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black)),
                              content: RichText(
                                text: TextSpan(
                                  children: [
                                    for (var i = 0; i < allocationsList.length; i++) ...[
                                      TextSpan(
                                        text: "\n" + allocationsList[i].name.toUpperCase() + "\n",
                                        style: boldTextStyle(color: Colors.black),
                                      ),
                                      TextSpan(
                                        text: allocationsList[i].usable_remaining_leaves + " " + allocationsList[i].request_unit.capitalizeFirstLetter() + 's Available' + "\n" +
                                              "Valid Until " + allocationsList[i].expire_date +  "\n",
                                        style: TextStyle(color: Colors.black),
                                      ),
                                    ]
                                  ]
                                ),
                              ),
                              actions: [
                                TextButton(
                                  style: ButtonStyle(
                                    shape: MaterialStateProperty.all<RoundedRectangleBorder>(
                                      RoundedRectangleBorder(
                                        borderRadius: BorderRadius.circular(0.0),
                                      ),
                                    ),
                                    backgroundColor: MaterialStateProperty.all<Color>(
                                      Color(0xffe4aa4c),
                                    ),
                                  ),
                                  child: Text(
                                    "OK",
                                    style: TextStyle(color: Colors.black),
                                  ),
                                  onPressed: () {
                                    Navigator.pop(context);
                                  },
                                ),
                              ],
                            ),
                          );
                        },
                        transitionBuilder: (context, animation1, animation2, child) {
                          const begin = Offset(0.0, -1.0);
                          const end = Offset.zero;
                          const curve = Curves.easeInOut;
                          var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                          var offsetAnimation = animation1.drive(tween);
                          return SlideTransition(position: offsetAnimation, child: child);
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
          ),
          Expanded(child: PagedVerticalCalendar(
            listPadding: EdgeInsets.symmetric( vertical: 28),
            startWeekWithSunday: true,
            addAutomaticKeepAlives: true,
            monthBuilder: (context, month, year) {
              return Column(
                children: [
                  /// create a customized header displaying the month and year
                  Container(
                    width: double.infinity,
                    alignment: Alignment.center,
                    padding: EdgeInsets.symmetric(vertical: 8),
                    decoration: BoxDecoration(
                      color: Color.fromARGB(255, 237, 237, 237),
                    ),
                    child: Text(
                      DateFormat('MMMM yyyy').format(DateTime(year, month)),
                      style: boldTextStyle(size: 24),
                    ),
                  ),

                  /// add a row showing the weekdays
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 20.0),
                    child: Row(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        weekText(AppLocalizations.of(context)!.sun),
                        weekText(AppLocalizations.of(context)!.mon),
                        weekText(AppLocalizations.of(context)!.tue),
                        weekText(AppLocalizations.of(context)!.wed),
                        weekText(AppLocalizations.of(context)!.thu),
                        weekText(AppLocalizations.of(context)!.fri),
                        weekText(AppLocalizations.of(context)!.sat),
                      ],
                    ),
                  ),
                ],
              );
            },
            dayBuilder: (context, date) {
              final eventsThisDay = timeOffList.where((e) => date.isSameDayOrAfter(e.date_from) && date.isSameDayOrBefore(e.date_to));

              return Container(
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  shape: BoxShape.rectangle,
                  //TODO: change color based on state
                  //TODO: add holidays and weekends and stress days
                  color: eventsThisDay.isEmpty ? Colors.transparent : 
                    (eventsThisDay.first.state == 'validate' ? Colors.green : 
                      (eventsThisDay.first.state == 'confirm' ? Colors.orange : Colors.grey)
                    ),
                ),
                child: 
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Container(
                        
                        child: Text(
                          DateFormat('d').format(date), style: boldTextStyle(),
                        ),
                      ),
                    ],
                  ),
                
              );
            },
            onDayPressed: (day) {
              print(day);
              final eventsThisDay = timeOffList.where((e) => day.isSameDayOrAfter(e.date_from) && day.isSameDayOrBefore(e.date_to));
              print('items this day:');
              print(eventsThisDay);
              if (!eventsThisDay.isEmpty) {
                if(eventsThisDay.length > 1){
                  print('more than 1 item in this day');
                }else {
                  print('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa');
                  print(eventsThisDay.first.holiday_status_id);
                  Navigator.push(
                    context,
                    PageRouteBuilder(
                      transitionDuration: Duration(milliseconds: 375), // Adjust the duration as needed
                      pageBuilder: (context, animation, secondaryAnimation) => CreateEditTimeoff(timeOffEdit: eventsThisDay.first),
                      transitionsBuilder: (context, animation, secondaryAnimation, child) {
                        const begin = Offset(1.0, 0.0);
                        const end = Offset.zero;
                        const curve = Curves.easeInOut;

                        var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                        var offsetAnimation = animation.drive(tween);

                        return SlideTransition(position: offsetAnimation, child: child);
                      },
                    ),
                  );
                }
              }else {
                Navigator.push(
                    context,
                    PageRouteBuilder(
                      transitionDuration: Duration(milliseconds: 375), // Adjust the duration as needed
                      pageBuilder: (context, animation, secondaryAnimation) => CreateEditTimeoff(newStartDate: day, newEndDate: day),
                      transitionsBuilder: (context, animation, secondaryAnimation, child) {
                        const begin = Offset(1.0, 0.0);
                        const end = Offset.zero;
                        const curve = Curves.easeInOut;

                        var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                        var offsetAnimation = animation.drive(tween);

                        return SlideTransition(position: offsetAnimation, child: child);
                      },
                    ),
                  );
                // CreateEditTimeoff();
              }
            },
          ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            PageRouteBuilder(
              transitionDuration: Duration(milliseconds: 375), // Adjust the duration as needed
              pageBuilder: (context, animation, secondaryAnimation) => CreateEditTimeoff(),
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
                const begin = Offset(1.0, 0.0);
                const end = Offset.zero;
                const curve = Curves.easeInOut;

                var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                var offsetAnimation = animation.drive(tween);

                return SlideTransition(position: offsetAnimation, child: child);
              },
            ),
          ).then((value) => {
            if(value == true){
              getTimeOffList()
            }
          });
        },
        child: Image.asset('assets/images/icons/plus.png', height: 32, width: 32, color: black),
        backgroundColor: Color(0xffe4aa4c), // Set your desired color
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(0.0), // Adjust the border radius as needed
        ),
      ),
    );
  }

  Widget weekText(String text) {
    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: Text(
        text,
        style: TextStyle(color: Colors.grey, fontSize: 10),
      ),
    );
  }
}

I have already tried:

  • Ensuring that PageController is disposed of in the dispose method.
  • Checking for any asynchronous operations that might be trying to use the controller after it's disposed.
  • Searching for any indirect usage of PagingController in my codebase or third-party packages.
  • Any insights or suggestions on what might be causing this exception and how to resolve it would be greatly appreciated.
0

There are 0 answers