Let me explain my Flutter structure first. I have a flutter main application and another application added as a package that has a different routing method and navigation. app behavior is when I click on a card on the main app it will get me to the package app, but when I go back to the home interface which is the main app. I'm getting the following error.
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
A RouteState was used after being disposed.
What I have tried so far I have tried to observe the navigation stack using route_observer_mixin but, it didn't work because I have two different navigations in the main app and the package.
if I try to remove the RouteState.dispose() in the package the error is gone, but that is a bad practice, right? because the memory leak could happen.
I'll put the related code section below for your reference.
Code section from main project main.dart file
class GalleryApp extends StatefulWidget {
// GalleryApp({super.key});
GalleryApp({
super.key,
this.initialRoute,
this.isTestMode = false,
});
late final String? initialRoute;
late final bool isTestMode;
final _auth = CampusAppsPortalAuth();
@override
State<GalleryApp> createState() => _GalleryAppState();
}
RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class _GalleryAppState extends State<GalleryApp> {
late final String loginRoute = '/signin';
get isTestMode => false;
@override
Widget build(BuildContext context) {
return ModelBinding(
initialModel: GalleryOptions(
themeMode: ThemeMode.system,
textScaleFactor: systemTextScaleFactorOption,
customTextDirection: CustomTextDirection.localeBased,
locale: null,
timeDilation: timeDilation,
platform: defaultTargetPlatform,
isTestMode: isTestMode,
),
child: Builder(
builder: (context) {
final options = GalleryOptions.of(context);
return MaterialApp(
restorationScopeId: 'rootGallery',
title: 'Flutter Gallery',
debugShowCheckedModeBanner: false,
navigatorObservers: [routeObserver],
themeMode: options.themeMode,
theme: GalleryThemeData.lightThemeData.copyWith(
platform: options.platform,
),
darkTheme: GalleryThemeData.darkThemeData.copyWith(
platform: options.platform,
),
localizationsDelegates: const [
...GalleryLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate()
],
initialRoute: loginRoute,
supportedLocales: GalleryLocalizations.supportedLocales,
locale: options.locale,
localeListResolutionCallback: (locales, supportedLocales) {
deviceLocale = locales?.first;
return basicLocaleListResolution(locales, supportedLocales);
},
onGenerateRoute: (settings) {
return RouteConfiguration.onGenerateRoute(settings);
},
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) =>
Scaffold(body: Center(child: Text('Not Found'))),
);
});
},
),
);
}
}
class RootPage extends StatelessWidget {
const RootPage({
super.key,
});
@override
Widget build(BuildContext context) {
return const ApplyTextOptions(
child: SplashPage(
child: Backdrop(),
),
);
}
}
Code section from main project Backdrop
class Backdrop extends StatefulWidget {
const Backdrop({super.key, this.settingsPage, this.homePage, this.loginPage});
final Widget? settingsPage;
final Widget? homePage;
final Widget? loginPage;
@override
State<Backdrop> createState() => _BackdropState();
}
RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class _BackdropState extends State<Backdrop>
with TickerProviderStateMixin, RouteAware {
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute<dynamic>);
}
late AnimationController _settingsPanelController;
late AnimationController _iconController;
late FocusNode _settingsPageFocusNode;
late ValueNotifier<bool> _isSettingsOpenNotifier;
late Widget _settingsPage;
late Widget _homePage;
late Widget _unknownPage;
@override
void initState() {
super.initState();
_settingsPanelController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_iconController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_settingsPageFocusNode = FocusNode();
_isSettingsOpenNotifier = ValueNotifier(false);
_settingsPage = widget.settingsPage ??
SettingsPage(
animationController: _settingsPanelController,
);
_homePage = widget.homePage ?? const HomePage();
_unknownPage = widget.homePage ?? const HomePage();
}
@override
void dispose() {
_settingsPanelController.dispose();
_iconController.dispose();
_settingsPageFocusNode.dispose();
_isSettingsOpenNotifier.dispose();
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPush() {
final route = ModalRoute.of(context)!.settings.name;
print('didPush route: $route');
}
@override
void didPopNext() {
final route = ModalRoute.of(context)!.settings.name;
print('didPopNext route: $route');
}
@override
void didPushNext() {
final route = ModalRoute.of(context)!.settings.name;
print('didPushNext route: $route');
}
@override
void didPop() {
final route = ModalRoute.of(context)!.settings.name;
print('didPop route: $route');
}
void _toggleSettings() {
// Animate the settings panel to open or close.
if (_isSettingsOpenNotifier.value) {
_settingsPanelController.reverse();
_iconController.reverse();
} else {
_settingsPanelController.forward();
_iconController.forward();
}
_isSettingsOpenNotifier.value = !_isSettingsOpenNotifier.value;
}
Animation<RelativeRect> _slideDownSettingsPageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0),
end: const RelativeRect.fromLTRB(0, 0, 0, 0),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Animation<RelativeRect> _slideDownHomePageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: const RelativeRect.fromLTRB(0, 0, 0, 0),
end: RelativeRect.fromLTRB(
0,
constraints.biggest.height - galleryHeaderHeight,
0,
-galleryHeaderHeight,
),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final isDesktop = isDisplayDesktop(context);
bool signedIn = campusAppsPortalInstance.getSignedIn();
log('signedIn: $signedIn! ');
print('signedIn: $signedIn!');
log('is decktop $isDesktop');
final Widget settingsPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: !isSettingsOpen,
child: isSettingsOpen
? RawKeyboardListener(
includeSemantics: false,
focusNode: _settingsPageFocusNode,
onKey: (event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
_toggleSettings();
}
},
child: FocusScope(child: _settingsPage),
)
: ExcludeFocus(child: _settingsPage),
);
},
);
final Widget homePage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _homePage),
);
},
);
final Widget unknownPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _unknownPage),
);
},
);
final Widget loginPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(
child: LoginPage(
// onSignIn: (credentials) async {
// var signedIn = await authState.signIn(
// credentials.username, credentials.password);
// if (signedIn) {
// await routeState.go('/gallery');
// }
// },
),
),
);
},
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: GalleryOptions.of(context).resolvedSystemUiOverlayStyle(),
child: Stack(
children: [
if (!isDesktop) ...[
// Slides the settings page up and down from the top of the
// screen.
PositionedTransition(
rect: _slideDownSettingsPageAnimation(constraints),
child: settingsPage,
),
// Slides the home page up and down below the bottom of the
// screen.
PositionedTransition(
rect: _slideDownHomePageAnimation(constraints),
child: homePage,
),
PositionedTransition(
rect: _slideDownHomePageAnimation(constraints),
child: loginPage,
),
],
if (isDesktop && signedIn) ...[
Semantics(sortKey: const OrdinalSortKey(2), child: homePage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
if (isSettingsOpen) {
return ExcludeSemantics(
child: Listener(
onPointerDown: (_) => _toggleSettings(),
child: const ModalBarrier(dismissible: false),
),
);
} else {
return Container();
}
},
),
Semantics(
sortKey: const OrdinalSortKey(3),
child: ScaleTransition(
alignment: Directionality.of(context) == TextDirection.ltr
? Alignment.topRight
: Alignment.topLeft,
scale: CurvedAnimation(
parent: _settingsPanelController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryContainer,
child: Container(
constraints: const BoxConstraints(
maxHeight: 560,
maxWidth: desktopSettingsWidth,
minWidth: desktopSettingsWidth,
),
child: settingsPage,
),
),
),
),
),
],
if (isDesktop && !signedIn) ...[
Semantics(sortKey: const OrdinalSortKey(2), child: loginPage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
if (isSettingsOpen) {
return ExcludeSemantics(
child: Listener(
onPointerDown: (_) => _toggleSettings(),
child: const ModalBarrier(dismissible: false),
),
);
} else {
return Container();
}
},
),
Semantics(
sortKey: const OrdinalSortKey(3),
child: ScaleTransition(
alignment: Directionality.of(context) == TextDirection.ltr
? Alignment.topRight
: Alignment.topLeft,
scale: CurvedAnimation(
parent: _settingsPanelController,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryContainer,
child: Container(
constraints: const BoxConstraints(
maxHeight: 560,
maxWidth: desktopSettingsWidth,
minWidth: desktopSettingsWidth,
),
child: settingsPage,
),
),
),
),
),
],
_SettingsIcon(
animationController: _iconController,
toggleSettings: _toggleSettings,
isSettingsOpenNotifier: _isSettingsOpenNotifier,
),
_LogoutIcon(
animationController: _iconController,
toggleSettings: _toggleSettings,
isSettingsOpenNotifier: _isSettingsOpenNotifier,
),
],
),
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: _buildStack,
);
}
}
Code from main project route file
class Path {
const Path(this.pattern, this.builder, {this.openInSecondScreen = false});
/// A RegEx string for route matching.
final String pattern;
/// The builder for the associated pattern route. The first argument is the
/// [BuildContext] and the second argument a RegEx match if that is included
/// in the pattern.
///
/// ```dart
/// Path(
/// 'r'^/demo/([\w-]+)$',
/// (context, matches) => Page(argument: match),
/// )
/// ```
final PathWidgetBuilder builder;
/// If the route should open on the second screen on foldables.
final bool openInSecondScreen;
}
class RouteConfiguration {
/// List of [Path] to for route matching. When a named route is pushed with
/// [Navigator.pushNamed], the route name is matched with the [Path.pattern]
/// in the list below. As soon as there is a match, the associated builder
/// will be returned. This means that the paths higher up in the list will
/// take priority.
static List<Path> paths = [
Path(
r'^' + DemoPage.baseRoute + r'/([\w-]+)$',
(context, match) => DemoPage(slug: match),
openInSecondScreen: false,
),
Path(
r'^' + rally_routes.homeRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(rally.loadLibrary,
() => rally.RallyApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + shrine_routes.homeRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(shrine.loadLibrary,
() => shrine.ShrineApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + shrine_routes.attendanceRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(
attendance.loadLibrary,
() => attendance
.CampusAttendanceManagementSystem()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + crane_routes.defaultRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(crane.loadLibrary,
() => crane.CraneApp(), // ignore: prefer_const_constructors
placeholder: const DeferredLoadingPlaceholder(name: 'Crane')),
),
openInSecondScreen: true,
),
Path(
r'^' + fortnightly_routes.defaultRoute,
(context, match) => StudyWrapper(
study: DeferredWidget(
fortnightly.loadLibrary,
// ignore: prefer_const_constructors
() => fortnightly.FortnightlyApp()),
),
openInSecondScreen: true,
),
Path(
r'^' + reply_routes.homeRoute,
// ignore: prefer_const_constructors
(context, match) =>
const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true),
openInSecondScreen: true,
),
Path(
r'^' + starter_app_routes.defaultRoute,
(context, match) => const StudyWrapper(
study: starter_app.StarterApp(),
),
openInSecondScreen: true,
),
Path(
r'^/',
(context, match) => const RootPage(),
openInSecondScreen: false,
),
Path(
r'^' + starter_app_routes.loginRoute,
(context, match) => const LoginPage(),
openInSecondScreen: false,
),
];
/// The route generator callback used when the app is navigated to a named
/// route. Set it on the [MaterialApp.onGenerateRoute] or
/// [WidgetsApp.onGenerateRoute] to make use of the [paths] for route
/// matching.
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
for (final path in paths) {
final regExpPattern = RegExp(path.pattern);
if (regExpPattern.hasMatch(settings.name!)) {
final firstMatch = regExpPattern.firstMatch(settings.name!)!;
final match = (firstMatch.groupCount == 1) ? firstMatch.group(1) : null;
if (kIsWeb) {
return NoAnimationMaterialPageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
}
if (path.openInSecondScreen) {
return TwoPanePageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
} else {
return MaterialPageRoute<void>(
builder: (context) => FutureBuilder<bool>(
future: isAuthorized(settings),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return path.builder(context, match);
}
return LoginPage();
},
),
settings: settings,
);
}
}
}
return null;
}
}
Code from package app.dart
class CampusAttendanceManagementSystem extends StatefulWidget {
const CampusAttendanceManagementSystem({super.key});
@override
State<CampusAttendanceManagementSystem> createState() =>
_CampusAttendanceManagementSystemState();
}
class _CampusAttendanceManagementSystemState
extends State<CampusAttendanceManagementSystem> {
final _auth = SMSAuth();
final _navigatorKey = GlobalKey<NavigatorState>();
late final RouteState _routeState;
late final SimpleRouterDelegate _routerDelegate;
late final TemplateRouteParser _routeParser;
@override
void initState() {
/// Configure the parser with all of the app's allowed path templates.
_routeParser = TemplateRouteParser(
allowedPaths: [
'/signin',
'/avinya_types/new',
'/avinya_types/all',
'/avinya_types/popular',
'/avinya_type/:id',
'/avinya_type/new',
'/avinya_type/edit',
'/activities/new',
'/activities/all',
'/activities/popular',
'/activity/:id',
'/activity/new',
'/activity/edit',
'/attendance_marker',
'/#access_token',
],
guard: _guard,
initialRoute: '/signin',
);
_routeState = RouteState(_routeParser);
_routerDelegate = SimpleRouterDelegate(
routeState: _routeState,
navigatorKey: _navigatorKey,
builder: (context) => SMSNavigator(
navigatorKey: _navigatorKey,
),
);
// Listen for when the user logs out and display the signin screen.
_auth.addListener(_handleAuthStateChanged);
super.initState();
}
@override
Widget build(BuildContext context) => RouteStateScope(
notifier: _routeState,
child: SMSAuthScope(
notifier: _auth,
child: MaterialApp.router(
routerDelegate: _routerDelegate,
routeInformationParser: _routeParser,
// Revert back to pre-Flutter-2.5 transition behavior:
// https://github.com/flutter/flutter/issues/82053
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
},
),
),
),
),
);
Future<ParsedRoute> _guard(ParsedRoute from) async {
final signedIn = await _auth.getSignedIn();
// String? jwt_sub = campusAttendanceSystemInstance.getJWTSub();
final signInRoute = ParsedRoute('/signin', '/signin', {}, {});
final avinyaTypesRoute =
ParsedRoute('/avinya_types', '/avinya_types', {}, {});
final activitiesRoute = ParsedRoute('/activities', '/activities', {}, {});
final attendanceMarkerRoute =
ParsedRoute('/attendance_marker', '/attendance_marker', {}, {});
// // Go to /apply if the user is not signed in
log("_guard signed in $signedIn");
// log("_guard JWT sub ${jwt_sub}");
log("_guard from ${from.toString()}\n");
if (signedIn && from == avinyaTypesRoute) {
return avinyaTypesRoute;
} else if (signedIn && from == activitiesRoute) {
return activitiesRoute;
} else if (signedIn && from == attendanceMarkerRoute) {
return attendanceMarkerRoute;
}
// Go to /application if the user is signed in and tries to go to /signin.
else if (signedIn && from == signInRoute) {
return ParsedRoute('/avinya_types', '/avinya_types', {}, {});
}
log("_guard signed in2 $signedIn");
// else if (signedIn && jwt_sub != null) {
// return avinyaTypesRoute;
// }
return from;
}
void _handleAuthStateChanged() async {
bool signedIn = await _auth.getSignedIn();
log("_handleAuthStateChanged signed in $signedIn");
if (!signedIn) {
_routeState.go('/signin');
}
}
@override
void dispose() {
_auth.removeListener(_handleAuthStateChanged);
_routeState.dispose();
_routerDelegate.dispose();
super.dispose();
}
}
Please make any suggestions to fix this issue. Thanks in advance