Related
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
I am working with nested navigators for multiple sections in my app and I have defined the keys for these navigators, you can see below the code where I have added Navigator keys in class so i can use it in my project by accessing class
class NavigatorKeys {
static final GlobalKey<NavigatorState> homeNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: "homeNavigatorKey");
static final GlobalKey<NavigatorState> shiftNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: "shiftNavigatorKey");
static final GlobalKey<NavigatorState> requestNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: "requestNavigatorKey");
static final GlobalKey<NavigatorState> messageNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: "messageNavigatorKey");
static final GlobalKey<NavigatorState> profileNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: "ProfileNavigatorKey");
}
Below is the code for one of the sections i.e ProfileNavigator which uses key
import '/theme/styles.dart';
import '/utils/constants.dart';
import '/views/profile/balances_page.dart';
import '/views/profile/change_password_page.dart';
import '/views/profile/language_page.dart';
import '/views/profile/profile_page.dart';
import '/views/profile/test_page.dart';
import '/views/profile/wage_accounts.dart';
import '/widgets/page_route_builder.dart';
import 'package:flutter/material.dart';
class ProfileNavigator extends StatelessWidget {
const ProfileNavigator({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Navigator(
key: NavigatorKeys.profileNavigatorKey,
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/':
return pageRouteBuilder(
ProfilePage(),
);
case '/langPage':
return pageRouteBuilder(
LanguagePage(),
);
case '/changePass':
return pageRouteBuilder(
ChangePasswordPage(),
);
case '/balancePage':
return pageRouteBuilder(
BalancesPage(),
);
case '/testPage':
return pageRouteBuilder(
TestPage(),
);
case '/balancePage':
return pageRouteBuilder(
BalancesPage(),
);
case '/wageAccounts':
return pageRouteBuilder(
WageAccountsPage(),
);
}
return pageRouteBuilder(
Container(
child: Center(
child: Text(
"Hmm...Thats Weird",
style: kTextStyleLargeBlack,
),
),
),
);
},
);
}
}
here is my implementation for BottomBar
List<Widget> mobilePages = [
HomeNavigator(),
GetConfigurationProvider().getConfigurationModel.shiftEnabled? ShiftNavigator():null,
RequestNavigator(),
GetConfigurationProvider().getConfigurationModel.messagesEnabled?MessageNavigator():null,
ProfileNavigator(),
];
Widget _bottomNavigatorBar() {
return Theme(
data: Theme.of(context).copyWith(
// sets the background color of the `BottomNavigationBar`
canvasColor: Theme.of(context).primaryColor,
// sets the active color of the `BottomNavigationBar` if `Brightness` is light
),
child: BottomBar(
height: Platform.isIOS ? 90 : 60,
backgroundColor: Theme.of(context).primaryColor,
duration: Duration(milliseconds: 800),
items: <BottomBarItem>[
BottomBarItem(
title: Text(pagesInfoList[0].pageName),
icon: Icon(pagesInfoList[0].pageIcon),
activeColor: Colors.white,
inactiveColor: Colors.grey[300]),
BottomBarItem(
title: Text(pagesInfoList[1].pageName),
icon: Icon(pagesInfoList[1].pageIcon),
activeColor: Colors.white,
inactiveColor: Colors.grey[300]),
BottomBarItem(
title: Text(pagesInfoList[2].pageName),
icon: Icon(pagesInfoList[2].pageIcon),
activeColor: Colors.white,
inactiveColor: Colors.grey[300]),
BottomBarItem(
title: Text(pagesInfoList[3].pageName),
icon: Icon(pagesInfoList[3].pageIcon),
activeColor: Colors.white,
inactiveColor: Colors.grey[300]),
BottomBarItem(
title: Text(pagesInfoList[4].pageName),
icon: Icon(pagesInfoList[4].pageIcon),
activeColor: Colors.white,
inactiveColor: Colors.grey[300]),
],
selectedIndex: _selectedIndex,
onTap: (int index) {
setState(() {
_selectedIndex = index;
});
}),
);
}
And below code I used for android back button to work
List<GlobalKey<NavigatorState>> _navigatorKeys = [
NavigatorKeys.homeNavigatorKey,
NavigatorKeys.shiftNavigatorKey,
NavigatorKeys.requestNavigatorKey,
NavigatorKeys.messageNavigatorKey,
NavigatorKeys.profileNavigatorKey,
];
Future<bool> _systemBackButtonPressed() {
if (_navigatorKeys[_selectedIndex].currentState.canPop()) {
_navigatorKeys[_selectedIndex]
.currentState
.pop(_navigatorKeys[_selectedIndex].currentContext);
} else {
SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop');
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _systemBackButtonPressed,
..........
So what happens is when I logout the user and Login again I start getting error of Duplicate Global Key detected it mostly initiates in ProfileNavigator, and then debug console keep showing the message of Duplicate Global key in infinite times
on Logout this is the code that take user to Login Page start of t
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (_) => LoginPage(),
),
);
In your Logout code, you have to use pushReplacement() instead of push(), because it will clear the stack and you will not get a duplicate key error in your widget tree.
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => LoginPage(),
),
);
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (_) => LoginPage(),
),
);
Places the new Scaffold on top of the previous. You can't push a 2nd duplicate Scaffold as a third layer when you want to return to the original screen, instead use:
Navigator.of(context).pop();
That removes the 2nd screen, and you just see the original, never really left 1st screen. If you don't want to keep the original screen under the 2nd, use
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => LoginPage(),
),
);
I want to know how can I show at the bottom of the screen a delete icon when I start dragging a Container in LongPressDraggable widget
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _onTap(context),
child: LongPressDraggable(
data: index,
maxSimultaneousDrags: 1,
onDragUpdate: (details) => print('update'),
onDragStarted: () => _buildDragTarget(),
onDragEnd: (_) => print('end'),
feedback: Material(
child: Container(
height: Sizes.height / 4.5,
width: Sizes.height / 4.5,
child: _DraggableContent(
index: index,
place: place,
),
),
),
childWhenDragging: Container(color: Colors.transparent),
child: _DraggableContent(
index: index,
place: place,
),
));
}
Widget _buildDragTarget() {
return DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return Icon(Icons.delete);
},
onAcceptWithDetails: (DragTargetDetails<int> dragTargetDetails) {
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
);
}
At the moment, when I start dragging the item, anything happens and I don't know how to continue
As far as I understood you need to show the delete icon on bottom when you are dragging an object.
You can wrap the delete icon with Visibility widget and set the visible parameter true or false based on the drag activity.
It will go something like this:
Widget _buildDragTarget() {
return DragTarget<int>(
builder: (BuildContext context, List<int> data, List<dynamic> rejects) {
return Visibility(
visible: isDragEnable,
child: Icon(Icons.delete));
},
onAcceptWithDetails: (DragTargetDetails<int> dragTargetDetails) {
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
);
}
You have an example with Dragable widget here : https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/
On the DROPPING AN ITEM part https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/#:~:text=the%20tomato%20image.-,Dropping%20an%20item,-At%20this%20point
You need to use the onAccept event on your DragTarget widget :
onAccept: (data) {
setState(() {
showSnackBarGlobal(context, 'Dropped successfully!');
_isDropped = true;
});
},
And on Drag starting, you can show up your delete icon by using this event (https://blog.logrocket.com/drag-and-drop-ui-elements-in-flutter-with-draggable-and-dragtarget/#:~:text=Listening%20to%20drag%20events) :
onDragStarted: () {
showSnackBarGlobal(context, 'Drag started');
},
I hope it will help you
I found the solution. It works creating a BlocProvider and working with the state.
class _Trash extends StatefulWidget {
const _Trash({
Key key,
}) : super(key: key);
#override
__TrashState createState() => __TrashState();
}
class __TrashState extends State<_Trash> with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
_listenerManager(bool isDragging) {
if (isDragging) {
setState(() => animation =
CurvedAnimation(parent: controller, curve: Curves.elasticOut));
controller.forward();
} else {
setState(() => animation = CurvedAnimation(
parent:
CurvedAnimation(parent: controller, curve: Interval(0.75, 1.0)),
curve: Curves.ease));
controller.reverse();
}
}
_deleteWidget(data) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.delete(data));
}
#override
void initState() {
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = CurvedAnimation(parent: controller, curve: Curves.elasticOut);
super.initState();
}
#override
Widget build(BuildContext context) {
return Positioned(
bottom: 0,
left: Sizes.width / 2 - Sizes.height / 15 / 2,
child: ScaleTransition(
scale: animation,
child: BlocListener<BoardBloc, BoardState>(
listenWhen: (previous, current) =>
previous.isDragging != current.isDragging,
listener: (context, state) => _listenerManager(state.isDragging),
child: Container(
height: Sizes.height / 15,
width: Sizes.height / 15,
color: Colors.amber,
child: DragTarget<int>(
builder: (BuildContext context, List<int> data,
List<dynamic> rejects) {
return Icon(Icons.delete);
},
onAcceptWithDetails:
(DragTargetDetails<int> dragTargetDetails) {
_deleteWidget(dragTargetDetails.data);
print('onAcceptWithDetails');
print('Data: ${dragTargetDetails.data}');
print('Offset: ${dragTargetDetails.offset}');
},
),
),
)));
}
}
class _PlaceTile extends StatelessWidget {
final int index;
final Place place;
const _PlaceTile({#required this.place, #required this.index});
_onTap(BuildContext context) => Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
PlaceViewer(index: index, animation: animation, place: place)));
_startDragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.startedDragging());
}
_stopDragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.stopDragging());
}
_dragging(context) {
BlocProvider.of<BoardBloc>(context).add(BoardEvent.dragging());
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _onTap(context),
child: LongPressDraggable(
data: index,
maxSimultaneousDrags: 1,
onDragUpdate: (details) => _dragging(context),
onDragStarted: () => _startDragging(context),
onDragEnd: (_) => _stopDragging(context),
feedback: Material(
child: Container(
height: Sizes.height / 4.5,
width: Sizes.height / 4.5,
child: _DraggableContent(
index: index,
place: place,
),
),
),
childWhenDragging: Container(color: Colors.transparent),
child: _DraggableContent(
index: index,
place: place,
),
));
}
}
I am learning flutter and i want to start animation & set App bar title 'syncing' from AlertDialog response(basically from other class) then end animation & set Title again after Async operation.
So currently I am achieving this using GlobalKey and Riverpod(StateNotifier).
Created MainScreen GlobalKey and using that GlobalKey from other class before Async Operation i am Calling
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.setSyncing();
and ending Animation after async operation:
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.syncProgressDone();
code :
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.setSyncing();
await SaveToLocal().save(context.read(listStateProvider.state));
await CloudNotes().updateCloudNote(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
mainScreenScaffoldKey.currentState.context
.read(syncProgressProvider)
.syncProgressDone();
}
and listening variable in AppBar title property in MainScreen
I feel this not right approach or is it?
here are some extra snippet
syncProgressProiver:
class SyncProgressModel extends StateNotifier<bool>{
SyncProgressModel() : super(false);
syncProgressDone(){
state =false;
}
setSyncing(){
state =true;
}
MainScreen AppBar Title
Consumer(
builder: (context, watch, child) {
var syncProgress = watch(syncProgressProvider.state);
if (!syncProgress) {
return const Text('To-Do List');
} else {
return Row(
children: [
const Text('Syncing..'),
Container(
margin: const EdgeInsets.only(left: 10),
width: 25,
height: 25,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: animColors,
),
)
],
);
}
},
),
Like this
I don't know anything about your animations (you don't actually share any of that logic or what initState are you referring to) but if the only thing you want is to animate the color of the CircularProgressIndicator then you could just create a StatefulWidget that does that for you and call it to build only when syncProgress == true
class AnimatedWidget extends StatefulWidget {
AnimatedWidget({Key key}) : super(key: key);
#override
_AnimatedWidgetState createState() => _AnimatedWidgetState();
}
class _AnimatedWidgetState extends State<AnimatedWidget>
with SingleTickerProviderStateMixin {
AnimationController _controller;
final Animatable<Color> _colorTween = TweenSequence<Color>([
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.red, end: Colors.amber),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.amber, end: Colors.green),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.green, end: Colors.blue),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.blue, end: Colors.purple),
weight: 20,
),
TweenSequenceItem<Color>(
tween: ColorTween(begin: Colors.purple, end: Colors.red),
weight: 20,
),
]).chain(CurveTween(curve: Curves.linear));
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
animationBehavior: AnimationBehavior.preserve,
vsync: this,
)..repeat();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
children: [
const Text('Syncing..'),
Container(
margin: const EdgeInsets.only(left: 10),
width: 25,
height: 25,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: _colorTween.animate(_controller)
),
)
],
);
}
}
and in your consumer just call it, the widget will handle the animation itself
Consumer(
builder: (context, watch, child) {
var syncProgress = watch(syncProgressProvider.state);
if (!syncProgress) {
return const Text('To-Do List');
} else {
return AnimatedWidget(); //right here
}
},
),
UPDATE
To safely refer to a widget's ancestor in its dispose() method, save a
reference to the ancestor by calling
dependOnInheritedWidgetOfExactType() in the widget's
didChangeDependencies() method.
What does this means is that you should keep a reference of the objects depending on your context before the context itself is rebuilt (when you call updateValueAt or updateValue in your dialog it rebuild the list and is no longer safe to call context.read)
updateCloudNote(BuildContext context) async {
/// keep the reference before calling the dialog to prevent
/// when the context change because of the dialogs action
final syncProgress = context.read(syncProgressProvider);
final listState = context.read(listStateProvider.state);
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
//when using GlobalKey didn't get that Widget Ancestor error
// use the reference saved instead of context.read
syncProgress.setSyncing();
await SaveToLocal().save(listState);
await CloudNotes().updateCloudNote(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
syncProgress.syncProgressDone();
}
}
What you did combining the providers is basically what you could do inside the same syncProgressProvider
final syncProgressProvider = StateNotifierProvider<SyncProgressModel>((ref)
=> SyncProgressModel(ref.read));
class SyncProgressModel extends StateNotifier<bool>{
final saveToLocal saveLocal = SaveToLocal();
final CloudNotes cloudNotes = CloudNotes();
final Reader _read;
SyncProgressModel(this._read) : super(false);
syncProgressDone(){
state = false;
}
setSyncing(){
state = true;
}
updateCall({int priority, int index, String task, String dateTime}) async {
state = true;
await saveLocal.save(_read(listStateProvider.state));
await cloudNotes.updateCloudNote(
task: task,
priority: priority,
dateTime: dateTime,
index: index,
);
state = false;
}
}
And finally combining the 2 ideas:
updateCloudNote(BuildContext context) async {
/// keep the reference before calling the dialog to prevent
/// when the context change because of the dialogs action
final syncProgress = context.read(syncProgressProvider);
Map<String, dynamic> dialogResponse = await showDialog(
context: context,
builder: (context) => EditNoteScreen(
_index,
_task,
_color,
dateTime,
priority: priority,
));
if (dialogResponse != null) {
await syncProgress.updateCall(
task: dialogResponse["task"],
priority: dialogResponse["priority"],
dateTime: dateTime.toString(),
index: dialogResponse["index"],
);
}
}
I'm developing a new Flutter Mobile app using the BLoC pattern . But I've got a problem and I don't find the solution yet.
The first one is my home page (with the MultiBlocProvider)
When I press on the FloatingActionButton.
It push a new screen to add a new "FicheMvt"
When I hit the add button.
It uses an onSave callback function to notify its parent of newly created "FicheMvt"
It gives me an error.
BlocProvider.of() called with a context that does not contain a Bloc
of type FicheMvtBloc.
No ancestor could be found starting from the context that was passed
to BlocProvider.of().
This can happen if the context you used comes from a widget above the
BlocProvider.
This is the home page (render 5 tab body)
class EtatCollecteScreen extends StatelessWidget {
final FicheMvtDAO ficheMvtDAO = FicheMvtDAO();
final FicheMvtReferenceDAO ficheMvtReferenceDAO = FicheMvtReferenceDAO();
#override
Widget build(BuildContext context) {
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
return MultiBlocProvider(
providers: [
BlocProvider<TabEtatCollecteBloc>(
create: (context) => TabEtatCollecteBloc(),
),
BlocProvider<FicheMvtBloc>(
create: (context) => FicheMvtBloc(
ficheMvtDAO: ficheMvtDAO,
)..add(FicheMvtRequested(idFiche: fiche.id)),
),
BlocProvider<FicheMvtReferenceBloc>(
create: (context) => FicheMvtReferenceBloc(
ficheMvtReferenceDAO: ficheMvtReferenceDAO,
)..add(FicheMvtReferenceRequested(idFiche: fiche.id)),
),
],
child: EtatCollecteContent(
ficheModel: fiche,
),
);
}
}
class EtatCollecteContent extends StatelessWidget {
final FicheModel ficheModel;
const EtatCollecteContent({Key key, #required this.ficheModel});
#override
Widget build(BuildContext context) {
return BlocBuilder<TabEtatCollecteBloc, EtatCollecteTab>(
builder: (context, activeTab) {
return Scaffold(
appBar: AppBar(
title: Text("${ficheModel.id} - ${ficheModel.description}"),
actions: <Widget>[
RefreshMvtButton(
visible: activeTab == EtatCollecteTab.completed,
ficheModel: ficheModel,
),
SendMvtButton(
visible: activeTab == EtatCollecteTab.uncommitted,
ficheModel: ficheModel,
),
],
),
body: EtatCollecteBody(
activeTab: activeTab,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return FicheMvtAddScreen(onSaveCallback: (idFiche, indicateurModel, codeSite) {
BlocProvider.of<FicheMvtBloc>(context).add(
FicheMvtAdded(
idFiche: idFiche,
indicateurModel: indicateurModel,
codeSite: codeSite,
),
);
});
},
settings: RouteSettings(
arguments: ficheModel,
),
),
);
},
child: Icon(Icons.add),
tooltip: "Add",
),
bottomNavigationBar: TabEtatCollecteSelector(
activeTab: activeTab,
onTabSelected: (tab) => BlocProvider.of<TabEtatCollecteBloc>(context).add(TabEtatCollecteUpdated(tab)),
),
);
},
);
}
}
And this is the code of the form to add new "FicheMvt" which contains another block that manages the dynamic form (FicheMvtAddBloc).
typedef OnSaveCallback = Function(
int idFiche,
IndicateurModel indicateurModel,
String codeSite,
);
class FicheMvtAddScreen extends StatelessWidget {
final OnSaveCallback onSaveCallback;
const FicheMvtAddScreen({Key key, #required this.onSaveCallback}) : super(key: key);
#override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
final FicheMvtRepository ficheMvtRepository = FicheMvtRepository();
return Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text("${fiche.id} - ${fiche.description}"),
),
backgroundColor: Colors.white,
body: BlocProvider<FicheMvtAddBloc>(
create: (context) => FicheMvtAddBloc(
ficheMvtRepository: ficheMvtRepository,
idFiche: fiche.id,
)..add(NewFicheMvtFormLoaded(idFiche: fiche.id)),
child: FicheMvtAddBody(
ficheModel: fiche,
onSave: onSaveCallback,
),
),
);
}
}
This is the content of the form
class FicheMvtAddBody extends StatefulWidget {
final FicheModel ficheModel;
final OnSaveCallback onSave;
#override
_FicheMvtAddBodyState createState() => _FicheMvtAddBodyState();
FicheMvtAddBody({Key key, #required this.ficheModel, #required this.onSave});
}
class _FicheMvtAddBodyState extends State<FicheMvtAddBody> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
void _onIndicateurChanged(String indicateur) =>
BlocProvider.of<FicheMvtAddBloc>(context).add(NewFicheMvtIndicateurChanged(indicateur: indicateur));
void _onSiteChanged(String site) => BlocProvider.of<FicheMvtAddBloc>(context).add(NewFicheMvtSiteChanged(site: site));
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
final txtIndicateur = Text("Indicateur");
final txtSite = Text("Site");
return BlocBuilder<FicheMvtAddBloc, FicheMvtAddState>(
builder: (context, state) {
return Form(
key: _formKey,
child: Center(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0),
children: <Widget>[
SizedBox(height: 24.0),
txtIndicateur,
DropdownButtonFormField<String>(
isExpanded: true,
hint: Text("Choisissez l'indicateur"),
value: state.indicateur?.code ?? null,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
onChanged: (String newValue) {
_onIndicateurChanged(newValue);
},
items: state.indicateurs?.isNotEmpty == true
? state.indicateurs
.map((CodeDescriptionModel model) => DropdownMenuItem(value: model.code, child: Text(model.description)))
.toList()
: const [],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer l\'indicateur s\'il vous plait';
}
return null;
},
),
SizedBox(height: 24.0),
txtSite,
DropdownButtonFormField<String>(
isExpanded: true,
hint: Text("Choisissez le site"),
value: state.site?.code ?? null,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
onChanged: (String newValue) {
_onSiteChanged(newValue);
},
items: state.sites?.isNotEmpty == true
? state.sites.map((CodeDescriptionModel model) => DropdownMenuItem(value: model.code, child: Text(model.description))).toList()
: const [],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer le site s\'il vous plait';
}
return null;
},
),
Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onSave(
fiche.id,
state.indicateur,
state.site?.code ?? null,
);
Navigator.pop(context);
}
},
padding: EdgeInsets.all(12),
color: Colors.blue,
child: Text('Create', style: TextStyle(color: Colors.white)),
),
)
],
),
),
);
},
);
}
}
Thanks for your help
You are using the wrong context in onSaveCallback. Here is a simplified hierarchy of your widgets:
- MaterialApp
- EtatCollecteScreen
- MultiBlocProvider
- FicheMvtAddScreen
So in your onSaveCallback you are accessing the context of FicheMvtAddScreen and it's obvious from the hierarchy above that BlocProvider couldn't find the requested Bloc. It's easy to fix this:
MaterialPageRoute(
builder: (pageContext) {
return FicheMvtAddScreen(onSaveCallback: (idFiche, indicateurModel, codeSite) {
BlocProvider.of<FicheMvtBloc>(context).add(
FicheMvtAdded(
idFiche: idFiche,
indicateurModel: indicateurModel,
codeSite: codeSite,
),
);
});
},
settings: RouteSettings(
arguments: ficheModel,
),
),
I've renamed context variable to pageContext in route builder function (so it wouldn't shadow required context). Now BlocProvider should able to find requested Bloc by accessing right context.
Another way to fix is to put MultiBlocProvider higher in widgets hierarchy.