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.
integration/290-306-335
Strycher 2 days ago
parent 9aaebce77c
commit 4dbceb3366

@ -25,6 +25,11 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen> { class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false; 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 MeshCoreConnector _connector;
late final VoidCallback _connectionListener; late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
@ -42,12 +47,21 @@ class _ScannerScreenState extends State<ScannerScreen> {
} else if (_connector.state == MeshCoreConnectionState.connected && } else if (_connector.state == MeshCoreConnectionState.connected &&
_connector.activeTransport == MeshCoreTransportType.bluetooth && _connector.activeTransport == MeshCoreTransportType.bluetooth &&
isCurrentRoute && 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; _changedNavigation = true;
_routingIntoApp = true;
if (mounted) { if (mounted) {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => const ChannelsScreen()), .push(
); MaterialPageRoute(builder: (context) => const ChannelsScreen()),
)
.then((_) {
if (mounted) _routingIntoApp = false;
});
} }
} }
}; };

@ -68,21 +68,27 @@ class _AppShellState extends State<AppShell> {
/// System back, in priority order: /// System back, in priority order:
/// 1. an open drawer closes, /// 1. an open drawer closes,
/// 2. otherwise pop the route (a pushed chat returns to its list), /// 2. on a detail screen, pop back to the list it came from,
/// 3. otherwise hand back to the OS, so Android returns to the home /// 3. on a primary view, hand back to the OS so Android returns to the
/// screen or the previous app rather than sitting on a dead press. /// home screen or the previous app.
/// ///
/// Screens previously did this with `PopScope(canPop: !isConnected)`, which /// Step 3 must NOT pop, even though the route below can be popped. The
/// swallowed back entirely while connected: the drawer would not close and /// primary views sit on top of the scanner, so popping would dump a
/// the app would never background. /// 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() { void _handleBack() {
final scaffold = _scaffoldKey.currentState; final scaffold = _scaffoldKey.currentState;
if (scaffold?.isDrawerOpen ?? false) { if (scaffold?.isDrawerOpen ?? false) {
scaffold!.closeDrawer(); scaffold!.closeDrawer();
return; return;
} }
final isPrimaryView = widget.selectedIndex != null;
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
if (navigator.canPop()) { if (!isPrimaryView && navigator.canPop()) {
navigator.pop(); navigator.pop();
return; return;
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.