Fix: FlutterError on Snackbar Overlay#547
Fix: FlutterError on Snackbar Overlay#547SantamRC wants to merge 2 commits intoCircuitVerse:masterfrom
Conversation
✅ Deploy Preview for cv-mobile-app-web ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
WalkthroughThis change refactors the logout and snackbar handling across multiple files. The 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR addresses the “No Overlay widget found” FlutterError occurring during authentication flows by changing when/how snackbars are shown so they aren’t triggered while the overlay is not yet available (login) or after it has been torn down (logout).
Changes:
- Refactors
SnackBarUtilsto use a shared internal_show()that defers snackbar display via scheduling. - Updates login flow to navigate first and then show the success snackbar shortly after.
- Moves logout success snackbar display out of the viewmodel and into the drawer UI, using
ScaffoldMessengerStatecaptured before logout actions.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| lib/viewmodels/cv_landing_viewmodel.dart | Changes logout handler to return a bool for confirmation and removes snackbar side-effect from the viewmodel. |
| lib/utils/snackbar_utils.dart | Reworks snackbar implementation to a centralized helper and adds a ScaffoldMessenger-based variant. |
| lib/ui/views/authentication/login_view.dart | Adjusts login success flow to navigate before showing snackbar to avoid overlay timing issues. |
| lib/ui/components/cv_drawer.dart | Shows logout success snackbar via ScaffoldMessenger captured before logout-triggered navigation changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Future<bool> onLogoutPressed() async { | ||
| Get.back(); | ||
|
|
||
| var _dialogResponse = await _dialogService.showConfirmationDialog( | ||
| var response = await _dialogService.showConfirmationDialog( |
There was a problem hiding this comment.
SnackBarUtils is no longer used in this viewmodel (the logout success snackbar was removed), so the snackbar_utils.dart import at the top of the file should be removed to avoid an unused_import lint/analyzer warning.
| if (response?.confirmed ?? false) { | ||
| onLogout(); | ||
| selectedIndex = 0; | ||
| SnackBarUtils.showDark( | ||
| AppLocalizations.of(Get.context!)!.cv_logout_success, | ||
| AppLocalizations.of(Get.context!)!.cv_logout_success_acknowledgement, | ||
| ); | ||
| return true; |
There was a problem hiding this comment.
onLogout() is an async function but returns void, so it can't be awaited here. As a result, onLogoutPressed() returns true (and the UI can show a “logout success” snackbar) before Google sign-out/session clearing has actually finished. Consider changing onLogout() to return Future<void> and awaiting it here before returning true (and handle/signals errors if sign-out fails).
| if (Get.context == null) return; | ||
|
|
||
| if (Get.isSnackbarOpen) { | ||
| Get.closeCurrentSnackbar(); | ||
| } | ||
|
|
||
| Future.microtask(() { |
There was a problem hiding this comment.
The Get.context == null guard happens before scheduling the microtask, but Get.rawSnackbar() runs later and can still throw if the overlay/context becomes unavailable in the meantime (the race this PR is addressing). To make this robust, re-check the required context/overlay inside the scheduled callback and/or use a post-frame callback (WidgetsBinding.instance.addPostFrameCallback) instead of Future.microtask so it runs after the next build when the overlay is mounted.
| if (Get.context == null) return; | |
| if (Get.isSnackbarOpen) { | |
| Get.closeCurrentSnackbar(); | |
| } | |
| Future.microtask(() { | |
| WidgetsBinding.instance.addPostFrameCallback((_) { | |
| if (Get.context == null) { | |
| return; | |
| } | |
| if (Get.isSnackbarOpen) { | |
| Get.closeCurrentSnackbar(); | |
| } |
| final messenger = ScaffoldMessenger.of(context); | ||
| final logoutSuccess = AppLocalizations.of(context)!.cv_logout_success; | ||
| final logoutAck = AppLocalizations.of(context)!.cv_logout_success_acknowledgement; | ||
| final confirmed = await _model.onLogoutPressed(); | ||
| if (confirmed) { | ||
| messenger.showSnackBar( | ||
| SnackBar( | ||
| content: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Text( | ||
| logoutSuccess, | ||
| style: const TextStyle( | ||
| color: Colors.white, | ||
| fontWeight: FontWeight.bold, | ||
| ), | ||
| ), | ||
| Text(logoutAck, style: const TextStyle(color: Colors.white)), | ||
| ], | ||
| ), | ||
| backgroundColor: Colors.black.withValues(alpha: 0.85), | ||
| behavior: SnackBarBehavior.floating, | ||
| margin: const EdgeInsets.all(12), | ||
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||
| duration: const Duration(seconds: 2), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
This logout snackbar duplicates styling/markup that now also exists in SnackBarUtils (and is likely to be needed elsewhere). Consider centralizing this (e.g., add a SnackBarUtils.showDarkWithMessenger(ScaffoldMessengerState, ...) or similar) so the app has one consistent snackbar implementation and future style tweaks don't require editing multiple widgets.
| final messenger = ScaffoldMessenger.of(context); | |
| final logoutSuccess = AppLocalizations.of(context)!.cv_logout_success; | |
| final logoutAck = AppLocalizations.of(context)!.cv_logout_success_acknowledgement; | |
| final confirmed = await _model.onLogoutPressed(); | |
| if (confirmed) { | |
| messenger.showSnackBar( | |
| SnackBar( | |
| content: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| logoutSuccess, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| Text(logoutAck, style: const TextStyle(color: Colors.white)), | |
| ], | |
| ), | |
| backgroundColor: Colors.black.withValues(alpha: 0.85), | |
| behavior: SnackBarBehavior.floating, | |
| margin: const EdgeInsets.all(12), | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | |
| duration: const Duration(seconds: 2), | |
| ), | |
| ); | |
| final logoutSuccess = AppLocalizations.of(context)!.cv_logout_success; | |
| final logoutAck = AppLocalizations.of(context)!.cv_logout_success_acknowledgement; | |
| final confirmed = await _model.onLogoutPressed(); | |
| if (confirmed) { | |
| SnackBarUtils.showDark(context, logoutSuccess, logoutAck); |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/utils/snackbar_utils.dart (1)
23-51: Take a capturedScaffoldMessengerStatehere instead ofBuildContext.The safe value across drawer dismissal is the messenger, not the drawer
context. That’s whylib/ui/components/cv_drawer.dartstill has to inline the sameSnackBarbody instead of reusing this helper.Suggested API shape
- static void showDarkWithContext( - BuildContext context, + static void showDarkWithMessenger( + ScaffoldMessengerState messenger, String title, String message, ) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar(
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2a5d5ae4-377c-4273-9013-b0324ac99cd6
📒 Files selected for processing (4)
lib/ui/components/cv_drawer.dartlib/ui/views/authentication/login_view.dartlib/utils/snackbar_utils.dartlib/viewmodels/cv_landing_viewmodel.dart
| await _model.login(_email, _password); | ||
|
|
||
| // Capture strings BEFORE any async navigation (important) | ||
| final successTitle = AppLocalizations.of(context)!.login_success_title; | ||
| final successMessage = AppLocalizations.of(context)!.login_success_message; | ||
|
|
||
| final errorTitle = AppLocalizations.of(context)!.login_error; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the login_view.dart file
fd "login_view.dart" --type fRepository: CircuitVerse/mobile-app
Length of output: 110
🏁 Script executed:
# Get the content of the login view to understand the full context
cat -n lib/ui/views/authentication/login_view.dart | head -200Repository: CircuitVerse/mobile-app
Length of output: 7786
🏁 Script executed:
# Search for the mounted pattern usage in the codebase to see if this is a common practice
rg "if \(!mounted\)" --type dart -B 2 -A 2 | head -50Repository: CircuitVerse/mobile-app
Length of output: 49
🏁 Script executed:
# Check what class LoginView extends
ast-grep --pattern 'class LoginView $$$'Repository: CircuitVerse/mobile-app
Length of output: 49
Add a mounted guard after the login await.
After await _model.login(...) completes, the widget could be disposed if the user navigates away during the request. This makes the subsequent AppLocalizations.of(context) calls unsafe—the context becomes stale and the inherited-widget lookup can fail or crash.
Suggested fix
await _model.login(_email, _password);
+ if (!mounted) return;
// Capture strings BEFORE any async navigation (important)| Future<bool> onLogoutPressed() async { | ||
| Get.back(); | ||
|
|
||
| var _dialogResponse = await _dialogService.showConfirmationDialog( | ||
| var response = await _dialogService.showConfirmationDialog( | ||
| title: AppLocalizations.of(Get.context!)!.cv_logout, | ||
| description: AppLocalizations.of(Get.context!)!.cv_logout_confirmation, | ||
| confirmationTitle: | ||
| AppLocalizations.of(Get.context!)!.cv_logout_confirmation_button, | ||
| ); | ||
|
|
||
| if (_dialogResponse?.confirmed ?? false) { | ||
| if (response?.confirmed ?? false) { | ||
| onLogout(); | ||
| selectedIndex = 0; | ||
| SnackBarUtils.showDark( | ||
| AppLocalizations.of(Get.context!)!.cv_logout_success, | ||
| AppLocalizations.of(Get.context!)!.cv_logout_success_acknowledgement, | ||
| ); | ||
| return true; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let me locate and examine the cv_landing_viewmodel.dart file
cd /tmp && git ls-files | grep -i "cv_landing_viewmodel"Repository: CircuitVerse/mobile-app
Length of output: 135
🏁 Script executed:
# Examine the onLogoutPressed and onLogout methods
cat -n lib/viewmodels/cv_landing_viewmodel.dart | sed -n '40,100p'Repository: CircuitVerse/mobile-app
Length of output: 1935
🏁 Script executed:
# Check how onLogoutPressed is used in cv_drawer.dart
fd "cv_drawer" --type fRepository: CircuitVerse/mobile-app
Length of output: 137
🏁 Script executed:
# Examine how onLogoutPressed is used in cv_drawer.dart
cat -n lib/ui/components/cv_drawer.dart | grep -A 5 -B 5 "onLogoutPressed"Repository: CircuitVerse/mobile-app
Length of output: 1456
🏁 Script executed:
# Let me also check the full context around the logout snackbar to confirm the usage
cat -n lib/ui/components/cv_drawer.dart | sed -n '220,240p'Repository: CircuitVerse/mobile-app
Length of output: 1399
Await logout completion before returning success.
onLogoutPressed() gates the logout-success snackbar, but onLogout() is async void, so it returns immediately before GoogleSignIn.signOut() completes. If signout fails, the exception is swallowed and the method still returns true, causing the caller to show a success message even though logout failed.
Change onLogout() to return Future<void> and await it before resetting state and returning:
Suggested fix
- void onLogout() async {
+ Future<void> onLogout() async {
_storage.isLoggedIn = false;
_storage.currentUser = null;
_storage.token = null;
@@
- onLogout();
+ await onLogout();
selectedIndex = 0;
return true;
Fixes #
This fixes the FlutterError on Snackbar Overlay issue #545
Login issue: the snackbar was trying to show before the app's overlay (the layer that displays floating UI like snackbars) was fully ready. Fixed by wrapping the call in a
Future.microtaskso it waits for the overlay to be mounted.Logout issue: the snackbar was trying to show after the overlay was already destroyed. When logout fires, it clears the user session and triggers a navigation change that tears down the overlay — and GetX's snackbar queue runs too late, finding nothing to attach to. Fixed by capturing the
ScaffoldMessengerStatebefore logout starts, since it lives higher in the widget tree and survives navigation changes.Summary by CodeRabbit
Release Notes
New Features
Bug Fixes