Skip to content

Commit 4dbfba4

Browse files
gnpricechrisbobbe
authored andcommitted
nav: Provide a way to wait for the navigator to be mounted
1 parent eef57f7 commit 4dbfba4

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

lib/widgets/app.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
14
import 'package:flutter/material.dart';
5+
import 'package:flutter/scheduler.dart';
26
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
37

48
import '../model/localizations.dart';
@@ -13,17 +17,65 @@ import 'store.dart';
1317
class ZulipApp extends StatelessWidget {
1418
const ZulipApp({super.key, this.navigatorObservers});
1519

20+
/// Whether the app's widget tree is ready.
21+
///
22+
/// This begins as false. It transitions to true when the
23+
/// [GlobalStore] has been loaded and the [MaterialApp] has been mounted,
24+
/// and then remains true.
25+
static ValueListenable<bool> get ready => _ready;
26+
static ValueNotifier<bool> _ready = ValueNotifier(false);
27+
28+
/// The navigator for the whole app.
29+
///
30+
/// This is always the [GlobalKey.currentState] of [navigatorKey].
31+
/// If [navigatorKey] is already mounted, this future completes immediately.
32+
/// Otherwise, it waits for [ready] to become true and then completes.
33+
static Future<NavigatorState> get navigator {
34+
final state = navigatorKey.currentState;
35+
if (state != null) return Future.value(state);
36+
37+
assert(!ready.value);
38+
final completer = Completer<NavigatorState>();
39+
ready.addListener(() {
40+
assert(ready.value);
41+
completer.complete(navigatorKey.currentState!);
42+
});
43+
return completer.future;
44+
}
45+
1646
/// A key for the navigator for the whole app.
1747
///
1848
/// For code that exists entirely outside the widget tree and has no natural
1949
/// [BuildContext] of its own, this enables interacting with the app's
2050
/// navigation, by calling [GlobalKey.currentState] to get a [NavigatorState].
51+
///
52+
/// During the app's early startup, this key will not yet be mounted.
53+
/// It will always be mounted before [ready] becomes true,
54+
/// and naturally before any widgets are mounted which are part of the
55+
/// app's main UI managed by the navigator.
56+
///
57+
/// See also [navigator], to asynchronously wait for the navigator
58+
/// to be mounted.
2159
static final navigatorKey = GlobalKey<NavigatorState>();
2260

61+
/// Reset the state of [ZulipApp] statics, for testing.
62+
///
63+
/// TODO refactor this better, perhaps unify with ZulipBinding
64+
@visibleForTesting
65+
static void debugReset() {
66+
_ready.dispose();
67+
_ready = ValueNotifier(false);
68+
}
69+
2370
/// A list to pass through to [MaterialApp.navigatorObservers].
2471
/// Useful in tests.
2572
final List<NavigatorObserver>? navigatorObservers;
2673

74+
void _declareReady() {
75+
assert(navigatorKey.currentContext != null);
76+
_ready.value = true;
77+
}
78+
2779
@override
2880
Widget build(BuildContext context) {
2981
final theme = ThemeData(
@@ -64,6 +116,10 @@ class ZulipApp extends StatelessWidget {
64116
navigatorKey: navigatorKey,
65117
navigatorObservers: navigatorObservers ?? const [],
66118
builder: (BuildContext context, Widget? child) {
119+
if (!ready.value) {
120+
SchedulerBinding.instance.addPostFrameCallback(
121+
(_) => _declareReady());
122+
}
67123
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
68124
return child!;
69125
},

test/model/binding.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:test/fake.dart';
88
import 'package:url_launcher/url_launcher.dart' as url_launcher;
99
import 'package:zulip/model/binding.dart';
1010
import 'package:zulip/model/store.dart';
11+
import 'package:zulip/widgets/app.dart';
1112

1213
import 'test_store.dart';
1314

@@ -61,6 +62,7 @@ class TestZulipBinding extends ZulipBinding {
6162
/// should clean up by calling this method. Typically this is done using
6263
/// [addTearDown], like `addTearDown(testBinding.reset);`.
6364
void reset() {
65+
ZulipApp.debugReset();
6466
_resetStore();
6567
_resetLaunchUrl();
6668
_resetDeviceInfo();

0 commit comments

Comments
 (0)