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.