Skip to content

Commit 00eb740

Browse files
committed
app: Maintain that the navigator stack is never empty
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 90bd7cc commit 00eb740

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

lib/widgets/app.dart

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,13 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
223223
theme: themeData,
224224

225225
navigatorKey: ZulipApp.navigatorKey,
226-
navigatorObservers: widget.navigatorObservers ?? const [],
226+
navigatorObservers: [
227+
if (widget.navigatorObservers != null)
228+
...widget.navigatorObservers!,
229+
// This must be the last item to maintain the invariant
230+
// that the navigator stack is always non-empty.
231+
_EmptyStackNavigatorObserver(),
232+
],
227233
builder: (BuildContext context, Widget? child) {
228234
if (!ZulipApp.ready.value) {
229235
SchedulerBinding.instance.addPostFrameCallback(
@@ -246,6 +252,39 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
246252
}
247253
}
248254

255+
/// Pushes a route whenever the observed navigator stack becomes empty.
256+
class _EmptyStackNavigatorObserver extends NavigatorObserver {
257+
void _pushRouteIfEmptyStack() async {
258+
final navigator = await ZulipApp.navigator;
259+
bool isEmptyStack = true;
260+
// TODO: find a better way to inspect the navigator stack
261+
navigator.popUntil((route) {
262+
isEmptyStack = false;
263+
return true; // never actually pops
264+
});
265+
if (isEmptyStack) {
266+
unawaited(navigator.push(
267+
MaterialWidgetRoute(page: const ChooseAccountPage())));
268+
}
269+
}
270+
271+
@override
272+
void didRemove(Route<void> route, Route<void>? previousRoute) async {
273+
if (previousRoute == null) {
274+
// The route removed is the bottom-most one.
275+
_pushRouteIfEmptyStack();
276+
}
277+
}
278+
279+
@override
280+
void didPop(Route<void> route, Route<void>? previousRoute) async {
281+
if (previousRoute == null) {
282+
// The route popped is the bottom-most one.
283+
_pushRouteIfEmptyStack();
284+
}
285+
}
286+
}
287+
249288
class ChooseAccountPage extends StatelessWidget {
250289
const ChooseAccountPage({super.key});
251290

test/widgets/app_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter_test/flutter_test.dart';
66
import 'package:zulip/log.dart';
7+
import 'package:zulip/model/actions.dart';
78
import 'package:zulip/model/database.dart';
89
import 'package:zulip/widgets/app.dart';
910
import 'package:zulip/widgets/home.dart';
@@ -57,6 +58,39 @@ void main() {
5758
});
5859
});
5960

61+
group('_EmptyStackNavigatorObserver', () {
62+
late List<Route<void>> pushedRoutes;
63+
late List<Route<void>> removedRoutes;
64+
65+
Future<void> prepare(WidgetTester tester) async {
66+
addTearDown(testBinding.reset);
67+
68+
pushedRoutes = [];
69+
removedRoutes = [];
70+
final testNavObserver = TestNavigatorObserver();
71+
testNavObserver.onPushed = (route, prevRoute) => pushedRoutes.add(route);
72+
testNavObserver.onRemoved = (route, prevRoute) => removedRoutes.add(route);
73+
74+
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver]));
75+
await tester.pump(); // start to load account
76+
check(pushedRoutes).single.isA<WidgetRoute>().page.isA<HomePage>();
77+
pushedRoutes.clear();
78+
}
79+
80+
testWidgets('push route when removing last route on stack', (tester) async {
81+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
82+
await prepare(tester);
83+
84+
final future = logOutAccount(testBinding.globalStore, eg.selfAccount.id);
85+
await tester.pump(TestGlobalStore.removeAccountDuration);
86+
await future;
87+
check(testBinding.globalStore.takeDoRemoveAccountCalls())
88+
.single.equals(eg.selfAccount.id);
89+
check(removedRoutes).single.isA<WidgetRoute>().page.isA<HomePage>();
90+
check(pushedRoutes).single.isA<WidgetRoute>().page.isA<ChooseAccountPage>();
91+
});
92+
});
93+
6094
group('ChooseAccountPage', () {
6195
Future<void> setupChooseAccountPage(WidgetTester tester, {
6296
required List<Account> accounts,

0 commit comments

Comments
 (0)