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.
This commit is contained in:
parent
c104ec16ed
commit
024c05734b
1 changed files with 43 additions and 52 deletions
|
|
@ -20,6 +20,28 @@ Future<void> 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<NavigatorState>();
|
||||
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
/// 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<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
|
||||
return showDialog<T>(
|
||||
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<AuthGate> {
|
|||
}
|
||||
},
|
||||
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<AuthGate> {
|
|||
/// Shows a dialog so the user can type a new password after clicking
|
||||
/// the "Reset Password" link from their email.
|
||||
Future<void> _showResetPasswordDialog() async {
|
||||
if (!mounted) return;
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
context: navigatorKey.currentContext!,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const _ResetPasswordDialog(),
|
||||
);
|
||||
|
|
@ -142,9 +161,7 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
|
|||
Future<void> _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<LoginScreen> {
|
|||
// 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<LoginScreen> {
|
|||
Future<void> _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<LoginScreen> {
|
|||
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<HomeScreen> {
|
|||
|
||||
/// Opens the scanner, gets the hw_id, then queries the DB.
|
||||
Future<void> _openScanner() async {
|
||||
final hwId = await Navigator.of(context).push<String>(
|
||||
final hwId = await navigatorKey.currentState!.push<String>(
|
||||
MaterialPageRoute(builder: (_) => const ScannerScreen()),
|
||||
);
|
||||
|
||||
if (hwId == null || !mounted) return;
|
||||
if (hwId == null) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
|
|
@ -408,32 +411,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
.eq('hw_id', hwId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
|
||||
if (data != null) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
await showGlobalDialog(
|
||||
builder: (_) => _AlreadyExistsDialog(hwId: hwId),
|
||||
);
|
||||
} else {
|
||||
// Dialog returns true if the car was added successfully.
|
||||
final added = await showDialog<bool>(
|
||||
context: context,
|
||||
final added = await showGlobalDialog<bool>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue