From 024c05734b1cda7654af64065090f30fcf92f006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCllner?= Date: Mon, 23 Feb 2026 09:04:00 +0100 Subject: [PATCH] fix: use global navigator/scaffold keys for all dialogs and snackbars Eliminates all _dependents.isEmpty and Duplicate GlobalKeys crashes by routing every dialog through a global NavigatorState key and every snackbar through a global ScaffoldMessengerState key. These keys live on MaterialApp and are never disposed, so async operations can safely show UI feedback regardless of widget tree rebuilds from auth events. --- lib/main.dart | 95 +++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 5b676aa..1dec1e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,28 @@ Future main() async { /// Convenience accessor used throughout the app. final supabase = Supabase.instance.client; +/// Global keys so dialogs & snackbars survive widget-tree rebuilds. +final navigatorKey = GlobalKey(); +final scaffoldMessengerKey = GlobalKey(); + +/// Show a snackbar safely through the global key. +void showGlobalSnackBar(String message, {bool isError = false}) { + scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: isError ? Colors.red : null, + ), + ); +} + +/// Show a dialog safely through the global navigator key. +Future showGlobalDialog({required WidgetBuilder builder}) { + return showDialog( + context: navigatorKey.currentContext!, + builder: builder, + ); +} + // ── Root App Widget ─────────────────────────────────────────────────── class HWHubApp extends StatelessWidget { const HWHubApp({super.key}); @@ -29,6 +51,8 @@ class HWHubApp extends StatelessWidget { return MaterialApp( title: 'HW Hub', debugShowCheckedModeBanner: false, + navigatorKey: navigatorKey, + scaffoldMessengerKey: scaffoldMessengerKey, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, @@ -82,10 +106,7 @@ class _AuthGateState extends State { } }, onError: (error) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Auth error: $error'), backgroundColor: Colors.red), - ); + showGlobalSnackBar('Auth error: $error', isError: true); }, ); @@ -96,10 +117,8 @@ class _AuthGateState extends State { /// Shows a dialog so the user can type a new password after clicking /// the "Reset Password" link from their email. Future _showResetPasswordDialog() async { - if (!mounted) return; - await showDialog( - context: context, + context: navigatorKey.currentContext!, barrierDismissible: false, builder: (_) => const _ResetPasswordDialog(), ); @@ -142,9 +161,7 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> { Future _save() async { final newPassword = _passwordController.text.trim(); if (newPassword.length < 6) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Password must be at least 6 characters.')), - ); + showGlobalSnackBar('Password must be at least 6 characters.'); return; } @@ -156,15 +173,11 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> { ); if (!mounted) return; Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Password updated successfully!')), - ); + showGlobalSnackBar('Password updated successfully!'); } on AuthException catch (e) { if (!mounted) return; setState(() => _isSaving = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), - ); + showGlobalSnackBar(e.message, isError: true); } } @@ -230,9 +243,7 @@ class _LoginScreenState extends State { // AuthGate listener will pick up the new session automatically. } on AuthException catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), - ); + showGlobalSnackBar(e.message, isError: true); } finally { if (mounted) setState(() => _isLoading = false); } @@ -244,9 +255,7 @@ class _LoginScreenState extends State { Future _forgotPassword() async { final email = _emailController.text.trim(); if (email.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Enter your email first.')), - ); + showGlobalSnackBar('Enter your email first.'); return; } @@ -255,15 +264,9 @@ class _LoginScreenState extends State { email, redirectTo: 'hwcollector://login', ); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Password reset email sent! Check your inbox.')), - ); + showGlobalSnackBar('Password reset email sent! Check your inbox.'); } on AuthException catch (e) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), - ); + showGlobalSnackBar(e.message, isError: true); } } @@ -393,11 +396,11 @@ class _HomeScreenState extends State { /// Opens the scanner, gets the hw_id, then queries the DB. Future _openScanner() async { - final hwId = await Navigator.of(context).push( + final hwId = await navigatorKey.currentState!.push( MaterialPageRoute(builder: (_) => const ScannerScreen()), ); - if (hwId == null || !mounted) return; + if (hwId == null) return; setState(() => _isBusy = true); @@ -408,32 +411,23 @@ class _HomeScreenState extends State { .eq('hw_id', hwId) .maybeSingle(); - if (!mounted) return; - setState(() => _isBusy = false); + if (mounted) setState(() => _isBusy = false); if (data != null) { - await showDialog( - context: context, + await showGlobalDialog( builder: (_) => _AlreadyExistsDialog(hwId: hwId), ); } else { - // Dialog returns true if the car was added successfully. - final added = await showDialog( - context: context, + final added = await showGlobalDialog( builder: (_) => _AddCarDialog(hwId: hwId), ); - if (added == true && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('$hwId added to your collection! 🎉')), - ); + if (added == true) { + showGlobalSnackBar('$hwId added to your collection! 🎉'); } } } catch (e) { - if (!mounted) return; - setState(() => _isBusy = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('DB error: $e'), backgroundColor: Colors.red), - ); + if (mounted) setState(() => _isBusy = false); + showGlobalSnackBar('DB error: $e', isError: true); } } } @@ -480,14 +474,11 @@ class _AddCarDialogState extends State<_AddCarDialog> { 'user_id': supabase.auth.currentUser!.id, }); if (!mounted) return; - // Return true to signal success — snackbar shown by HomeScreen. Navigator.of(context).pop(true); } catch (e) { if (!mounted) return; setState(() => _isAdding = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to add: $e'), backgroundColor: Colors.red), - ); + showGlobalSnackBar('Failed to add: $e', isError: true); } }