From 4dbceb3366c48f35c7d392f4e7ce1206226e9fb6 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 00:48:16 -0400 Subject: [PATCH] fix(#291): back must not drop a connected user on the radio list Two bugs, reported together from an S25 FE test. **Back popped to the scanner.** The previous commit made back pop whenever the route could be popped. The primary views sit on top of the scanner, so from the channel list back landed on the radio-connect screen. That is a regression I introduced: the `PopScope(canPop: !isConnected)` I removed existed to prevent exactly this, and I removed it without establishing why it was there. Back now distinguishes the two cases. A primary view (one carrying the bottom bar) hands back to the OS and backgrounds the app. A detail screen (a channel chat, which has no selectedIndex and is genuinely pushed) pops to its list. Reaching the scanner is what Disconnect is for, not what Back is for. **The scanner was a dead end while connected.** It only routed into the app on a connection transition, behind a one-shot flag that never reset while connected. So once showing with a live connection it stayed there, and pressing Connect did nothing, since there was nothing left to connect. It now routes in whenever it is the visible route and a connection is live, guarded against double-push by a dedicated flag rather than reusing _changedNavigation, which separately gates the disconnect-on-dispose cleanup. Known limitation, pre-existing and unchanged: this auto-entry is bluetooth only. USB and TCP connect from their own screens, which navigate themselves. flutter analyze clean, dart format clean, 469 tests pass. --- lib/screens/scanner_screen.dart | 22 ++++++++++++++++++---- lib/widgets/app_shell.dart | 20 +++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 3222168..f8eece9 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -25,6 +25,11 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; + + /// Re-entrancy guard for routing into the app. Distinct from + /// [_changedNavigation], which records that we entered at least once and + /// gates the disconnect-on-dispose cleanup. + bool _routingIntoApp = false; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; @@ -42,12 +47,21 @@ class _ScannerScreenState extends State { } else if (_connector.state == MeshCoreConnectionState.connected && _connector.activeTransport == MeshCoreTransportType.bluetooth && isCurrentRoute && - !_changedNavigation) { + !_routingIntoApp) { + // Route in whenever this screen is showing while connected, not just + // on the first connect. Otherwise landing back here with a live + // connection is a dead end: Connect does nothing, because there is + // nothing left to connect. _changedNavigation = true; + _routingIntoApp = true; if (mounted) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const ChannelsScreen()), - ); + Navigator.of(context) + .push( + MaterialPageRoute(builder: (context) => const ChannelsScreen()), + ) + .then((_) { + if (mounted) _routingIntoApp = false; + }); } } }; diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 689caa3..b95ba8d 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -68,21 +68,27 @@ class _AppShellState extends State { /// System back, in priority order: /// 1. an open drawer closes, - /// 2. otherwise pop the route (a pushed chat returns to its list), - /// 3. otherwise hand back to the OS, so Android returns to the home - /// screen or the previous app rather than sitting on a dead press. + /// 2. on a detail screen, pop back to the list it came from, + /// 3. on a primary view, hand back to the OS so Android returns to the + /// home screen or the previous app. /// - /// Screens previously did this with `PopScope(canPop: !isConnected)`, which - /// swallowed back entirely while connected: the drawer would not close and - /// the app would never background. + /// Step 3 must NOT pop, even though the route below can be popped. The + /// primary views sit on top of the scanner, so popping would dump a + /// connected user back onto the radio-connect list. Reaching the scanner is + /// what Disconnect is for, not what Back is for. + /// + /// A primary view is one carrying the bottom bar; a detail screen (a channel + /// chat) has no [selectedIndex] and is genuinely pushed. void _handleBack() { final scaffold = _scaffoldKey.currentState; if (scaffold?.isDrawerOpen ?? false) { scaffold!.closeDrawer(); return; } + + final isPrimaryView = widget.selectedIndex != null; final navigator = Navigator.of(context); - if (navigator.canPop()) { + if (!isPrimaryView && navigator.canPop()) { navigator.pop(); return; }