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.
|
/// Convenience accessor used throughout the app.
|
||||||
final supabase = Supabase.instance.client;
|
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 ───────────────────────────────────────────────────
|
// ── Root App Widget ───────────────────────────────────────────────────
|
||||||
class HWHubApp extends StatelessWidget {
|
class HWHubApp extends StatelessWidget {
|
||||||
const HWHubApp({super.key});
|
const HWHubApp({super.key});
|
||||||
|
|
@ -29,6 +51,8 @@ class HWHubApp extends StatelessWidget {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'HW Hub',
|
title: 'HW Hub',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
|
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
|
@ -82,10 +106,7 @@ class _AuthGateState extends State<AuthGate> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (!mounted) return;
|
showGlobalSnackBar('Auth error: $error', isError: true);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Auth error: $error'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -96,10 +117,8 @@ class _AuthGateState extends State<AuthGate> {
|
||||||
/// Shows a dialog so the user can type a new password after clicking
|
/// Shows a dialog so the user can type a new password after clicking
|
||||||
/// the "Reset Password" link from their email.
|
/// the "Reset Password" link from their email.
|
||||||
Future<void> _showResetPasswordDialog() async {
|
Future<void> _showResetPasswordDialog() async {
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: navigatorKey.currentContext!,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => const _ResetPasswordDialog(),
|
builder: (_) => const _ResetPasswordDialog(),
|
||||||
);
|
);
|
||||||
|
|
@ -142,9 +161,7 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
|
||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
final newPassword = _passwordController.text.trim();
|
final newPassword = _passwordController.text.trim();
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < 6) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar('Password must be at least 6 characters.');
|
||||||
const SnackBar(content: Text('Password must be at least 6 characters.')),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,15 +173,11 @@ class _ResetPasswordDialogState extends State<_ResetPasswordDialog> {
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar('Password updated successfully!');
|
||||||
const SnackBar(content: Text('Password updated successfully!')),
|
|
||||||
);
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isSaving = false);
|
setState(() => _isSaving = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar(e.message, isError: true);
|
||||||
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,9 +243,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
// AuthGate listener will pick up the new session automatically.
|
// AuthGate listener will pick up the new session automatically.
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar(e.message, isError: true);
|
||||||
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
@ -244,9 +255,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
Future<void> _forgotPassword() async {
|
Future<void> _forgotPassword() async {
|
||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
if (email.isEmpty) {
|
if (email.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar('Enter your email first.');
|
||||||
const SnackBar(content: Text('Enter your email first.')),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,15 +264,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
email,
|
email,
|
||||||
redirectTo: 'hwcollector://login',
|
redirectTo: 'hwcollector://login',
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
showGlobalSnackBar('Password reset email sent! Check your inbox.');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Password reset email sent! Check your inbox.')),
|
|
||||||
);
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
if (!mounted) return;
|
showGlobalSnackBar(e.message, isError: true);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,11 +396,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
|
||||||
/// Opens the scanner, gets the hw_id, then queries the DB.
|
/// Opens the scanner, gets the hw_id, then queries the DB.
|
||||||
Future<void> _openScanner() async {
|
Future<void> _openScanner() async {
|
||||||
final hwId = await Navigator.of(context).push<String>(
|
final hwId = await navigatorKey.currentState!.push<String>(
|
||||||
MaterialPageRoute(builder: (_) => const ScannerScreen()),
|
MaterialPageRoute(builder: (_) => const ScannerScreen()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hwId == null || !mounted) return;
|
if (hwId == null) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
|
@ -408,32 +411,23 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
.eq('hw_id', hwId)
|
.eq('hw_id', hwId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (mounted) setState(() => _isBusy = false);
|
||||||
setState(() => _isBusy = false);
|
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
await showDialog<void>(
|
await showGlobalDialog(
|
||||||
context: context,
|
|
||||||
builder: (_) => _AlreadyExistsDialog(hwId: hwId),
|
builder: (_) => _AlreadyExistsDialog(hwId: hwId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Dialog returns true if the car was added successfully.
|
final added = await showGlobalDialog<bool>(
|
||||||
final added = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => _AddCarDialog(hwId: hwId),
|
builder: (_) => _AddCarDialog(hwId: hwId),
|
||||||
);
|
);
|
||||||
if (added == true && mounted) {
|
if (added == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar('$hwId added to your collection! 🎉');
|
||||||
SnackBar(content: Text('$hwId added to your collection! 🎉')),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (mounted) setState(() => _isBusy = false);
|
||||||
setState(() => _isBusy = false);
|
showGlobalSnackBar('DB error: $e', isError: true);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('DB error: $e'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -480,14 +474,11 @@ class _AddCarDialogState extends State<_AddCarDialog> {
|
||||||
'user_id': supabase.auth.currentUser!.id,
|
'user_id': supabase.auth.currentUser!.id,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// Return true to signal success — snackbar shown by HomeScreen.
|
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _isAdding = false);
|
setState(() => _isAdding = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showGlobalSnackBar('Failed to add: $e', isError: true);
|
||||||
SnackBar(content: Text('Failed to add: $e'), backgroundColor: Colors.red),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue