Skip to content

fix(dashboard): make system back mirror the AppBar back arrow#244

Open
deaflynx wants to merge 3 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/dashboard-system-back-button
Open

fix(dashboard): make system back mirror the AppBar back arrow#244
deaflynx wants to merge 3 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/dashboard-system-back-button

Conversation

@deaflynx

Copy link
Copy Markdown
Contributor

Summary

The Android system back button (and iOS edge-swipe) now navigates through dashboard state exactly like the in-app AppBar back arrow, instead of immediately popping the route or showing the exit dialog.

Addresses the remaining system-back behavior reported in thingsboard/flutter_thingsboard_pe_app#285 — the AppBar arrow was already fixed in v1.8.0, but the hardware/gesture back still diverged.

Changes

  • Shared DashboardBackHandler — wraps each dashboard page in a PopScope and centralizes the back logic (close right layout → WebView goBack() → page-specific fallback). Reused by MainDashboardPage, HomeDashboardPage, SingleDashboardView, and FullscreenDashboardPage, so the AppBar arrow and system back share one code path.
  • Reactive TwoPageViewController.currentIndexMainDashboardPage now only intercepts back while the dashboard (not the dashboards list) is visible, letting the bottom-nav shell handle tab-switch/exit back as before.
  • Exit dialog removed — replaced the "Are you sure you want to exit?" confirmation dialogs with a direct app exit, matching standard Android apps (Gmail/YouTube/Slack) and Material guidance. Removed the now-unused areYouSureYouWantToExit / confirmToCloseTheApp l10n keys across all locales.

Test plan

  • Open a dashboard → system back walks through dashboard states, then closes the dashboard (matches AppBar arrow)
  • Dashboard with a right layout open → system back closes the panel first, then continues
  • Non-home tab (e.g. Alarms) → system back returns to Home tab; from Home → app exits (no dialog)
  • Home dashboard, single dashboard view, and fullscreen dashboard all behave consistently
  • Verify on both Android and iOS

The Android system back button (and iOS edge-swipe) now walks through
dashboard state the same way the in-app AppBar back arrow does, instead
of immediately popping the route or showing the exit dialog.

Introduces a shared DashboardBackHandler that wraps each dashboard page
in a PopScope and centralizes the back logic (close right layout ->
WebView goBack -> page-specific fallback), reused by MainDashboardPage,
HomeDashboardPage, SingleDashboardView and FullscreenDashboardPage.

TwoPageViewController now exposes a reactive currentIndex so
MainDashboardPage only intercepts back while the dashboard (not the
list) is visible, letting the bottom-nav shell handle tab/exit back.

Also replaces the "exit?" confirmation dialogs with a direct app exit
to match standard Android apps (Gmail/YouTube/Slack), and removes the
now-unused areYouSureYouWantToExit / confirmToCloseTheApp l10n keys.

@vvlladd28 vvlladd28 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Reviewed 36 changed files in fix(dashboard): make system back mirror the AppBar back arrow. The consolidation into DashboardBackHandler is a nice cleanup and the pageIndex == 1 gating cleanly delegates the dashboards-list back press to the bottom-nav shell. A few small concerns left inline, mostly around the fullscreen-dashboard back press, mounted checks after async gaps, and a couple of API/style polish points.


This is an auto-generated review. Findings may contain errors — please verify before applying changes.

}

Future<void> _handleBack() async {
await DashboardBackHandler.tryNavigateBack(_dashboardController);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discarding the bool from tryNavigateBack means that when the WebView can't go back and the right panel is closed, _handleBack is a no-op — but the wrapping DashboardBackHandler still has canPop: false, so the system back gesture is silently swallowed. Previously the gesture at least bubbled up to the now-removed exit dialog, so users had some feedback. Consider falling back to if (mounted && context.canPop()) Navigator.of(context).pop(); (mirroring single_dashboard_view.dart) when tryNavigateBack returns false — otherwise a fullscreen dashboard with no WebView history has no system-back exit at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed regression. Pre-PR this page had no PopScope, so the system back gesture popped the pushed GoRoute by default. The new DashboardBackHandler wraps the body with canPop: false, so when tryNavigateBack returns false (no right layout, no WebView history) the gesture was silently swallowed.

Fixed in 44b713c — added a Navigator.pop fallback after tryNavigateBack, mirroring SingleDashboardView._handleBack exactly.

return;
}
await widget.controller.closeDashboard();
_dashboardLoadingCtrl?.value = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the two awaits above, the widget may have been disposed. single_dashboard_view.dart guards its post-await work with a mounted check; this one writes _dashboardLoadingCtrl?.value = true (and implicitly relies on widget.controller still being valid) without one. If the user backs out fast enough — or if the route is replaced concurrently — this can fire on a stale state. A if (!mounted) return; between the await widget.controller.closeDashboard(); and the assignment would make this consistent with the other handlers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Added if (!mounted) return; between the await widget.controller.closeDashboard() and the _dashboardLoadingCtrl?.value = true write. Matches the SingleDashboardView pattern. Fixed in fc75a3c.


static Future<bool> tryNavigateBack(DashboardController? controller) async {
if (controller == null) return false;
if (controller.rightLayoutOpened.value == true) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: controller.rightLayoutOpened.value is already a bool, so the == true here (and on line 23's await web.canGoBack()-style check via the surrounding pattern) is redundant. The old call sites used ?.value == true because of the nullable receiver, but controller is already null-checked one line above. Just if (controller.rightLayoutOpened.value) reads cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — rightLayoutOpened is declared as a non-null ValueNotifier<bool> in DashboardController, so the == true is redundant. Dropped. Fixed in fc75a3c.

Comment thread lib/widgets/two_page_view.dart Outdated

class TwoPageViewController {
_TwoPageViewState? _state;
final ValueNotifier<int> currentIndex = ValueNotifier<int>(0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentIndex is exposed as a mutable ValueNotifier<int>, so any caller can call controller.currentIndex.value = 5 and silently desync it from _selectedIndex. Since the only intended public use is listening (see MainDashboardPage's ValueListenableBuilder<int>), consider exposing it as ValueListenable<int> publicly while keeping a private ValueNotifier<int> for internal updates — that locks the source of truth to _open/_close/onPageChanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — exposing the ValueNotifier publicly lets outside callers mutate the source of truth, which we don't want here. Made the backing field private (_currentIndex) and exposed a read-only ValueListenable<int> get currentIndex. The only external consumer (MainDashboardPage's ValueListenableBuilder<int>) is unaffected since it only reads. The three internal writers in _TwoPageViewState (_open, _close, onPageChanged) now go through the private field. Fixed in fc75a3c.

deaflynx added 2 commits May 28, 2026 17:15
- Guard MainDashboardPage._handleBack with a mounted check after
  awaiting closeDashboard, mirroring SingleDashboardView.
- Drop redundant `== true` on DashboardController.rightLayoutOpened.value.
- Expose TwoPageViewController.currentIndex as a read-only
  ValueListenable so external listeners cannot mutate the source.
DashboardBackHandler's PopScope (canPop: false) was swallowing the
system back gesture on FullscreenDashboardPage when tryNavigateBack
returned false (no right layout, no WebView history), leaving the user
stuck. Mirror SingleDashboardView._handleBack and fall back to
Navigator.pop so deep-linked / public-user entries can exit the page.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants