Skip to content

Commit

Permalink
nav: Provide a way to wait for the navigator to be mounted
Browse files Browse the repository at this point in the history
  • Loading branch information
gnprice authored and chrisbobbe committed Nov 6, 2023
1 parent eef57f7 commit 4dbfba4
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 0 deletions.
56 changes: 56 additions & 0 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';

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

/// Whether the app's widget tree is ready.
///
/// This begins as false. It transitions to true when the
/// [GlobalStore] has been loaded and the [MaterialApp] has been mounted,
/// and then remains true.
static ValueListenable<bool> get ready => _ready;
static ValueNotifier<bool> _ready = ValueNotifier(false);

/// The navigator for the whole app.
///
/// This is always the [GlobalKey.currentState] of [navigatorKey].
/// If [navigatorKey] is already mounted, this future completes immediately.
/// Otherwise, it waits for [ready] to become true and then completes.
static Future<NavigatorState> get navigator {
final state = navigatorKey.currentState;
if (state != null) return Future.value(state);

assert(!ready.value);
final completer = Completer<NavigatorState>();
ready.addListener(() {
assert(ready.value);
completer.complete(navigatorKey.currentState!);
});
return completer.future;
}

/// A key for the navigator for the whole app.
///
/// For code that exists entirely outside the widget tree and has no natural
/// [BuildContext] of its own, this enables interacting with the app's
/// navigation, by calling [GlobalKey.currentState] to get a [NavigatorState].
///
/// During the app's early startup, this key will not yet be mounted.
/// It will always be mounted before [ready] becomes true,
/// and naturally before any widgets are mounted which are part of the
/// app's main UI managed by the navigator.
///
/// See also [navigator], to asynchronously wait for the navigator
/// to be mounted.
static final navigatorKey = GlobalKey<NavigatorState>();

/// Reset the state of [ZulipApp] statics, for testing.
///
/// TODO refactor this better, perhaps unify with ZulipBinding
@visibleForTesting
static void debugReset() {
_ready.dispose();
_ready = ValueNotifier(false);
}

/// A list to pass through to [MaterialApp.navigatorObservers].
/// Useful in tests.
final List<NavigatorObserver>? navigatorObservers;

void _declareReady() {
assert(navigatorKey.currentContext != null);
_ready.value = true;
}

@override
Widget build(BuildContext context) {
final theme = ThemeData(
Expand Down Expand Up @@ -64,6 +116,10 @@ class ZulipApp extends StatelessWidget {
navigatorKey: navigatorKey,
navigatorObservers: navigatorObservers ?? const [],
builder: (BuildContext context, Widget? child) {
if (!ready.value) {
SchedulerBinding.instance.addPostFrameCallback(
(_) => _declareReady());
}
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context);
return child!;
},
Expand Down
2 changes: 2 additions & 0 deletions test/model/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:test/fake.dart';
import 'package:url_launcher/url_launcher.dart' as url_launcher;
import 'package:zulip/model/binding.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/widgets/app.dart';

import 'test_store.dart';

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

0 comments on commit 4dbfba4

Please sign in to comment.