navigator.dart 14.1 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
6
import 'overlay.dart';
7

Hixie's avatar
Hixie committed
8
abstract class Route<T> {
9 10 11 12
  /// The navigator that the route is in, if any.
  NavigatorState get navigator => _navigator;
  NavigatorState _navigator;

13 14 15
  List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];

  /// Called when the route is inserted into the navigator.
16
  ///
Hixie's avatar
Hixie committed
17 18 19 20
  /// Use this to populate overlayEntries and add them to the overlay
  /// (accessible as navigator.overlay). (The reason the Route is responsible
  /// for doing this, rather than the Navigator, is that the Route will be
  /// responsible for _removing_ the entries and this way it's symmetric.)
21 22
  ///
  /// The overlay argument will be null if this is the first route inserted.
Hixie's avatar
Hixie committed
23
  void install(OverlayEntry insertionPoint) { }
24 25 26

  /// Called after install() when the route is pushed onto the navigator.
  void didPush() { }
Hixie's avatar
Hixie committed
27

28 29 30
  /// Called after install() when the route replaced another in the navigator.
  void didReplace(Route oldRoute) { }

Hixie's avatar
Hixie committed
31 32 33 34
  /// A request was made to pop this route. If the route can handle it
  /// internally (e.g. because it has its own stack of internal state) then
  /// return false, otherwise return true. Returning false will prevent the
  /// default behavior of NavigatorState.pop().
35 36 37
  ///
  /// If this is called, the Navigator will not call dispose(). It is the
  /// responsibility of the Route to later call dispose().
Hixie's avatar
Hixie committed
38
  bool didPop(T result) => true;
39 40

  /// The given route has been pushed onto the navigator after this route.
41
  void didPushNext(Route nextRoute) { }
42 43

  /// The given route, which came after this one, has been popped off the
44 45 46
  /// navigator.
  void didPopNext(Route nextRoute) { }

47 48 49 50
  /// The given old route, which was the route that came after this one, has
  /// been replaced with the given new route.
  void didReplaceNext(Route oldNextRoute, Route newNextRoute) { }

51 52 53 54 55 56 57 58 59 60 61 62 63 64
  /// The route should remove its overlays and free any other resources.
  ///
  /// A call to didPop() implies that the Route should call dispose() itself,
  /// but it is possible for dispose() to be called directly (e.g. if the route
  /// is replaced, or if the navigator itself is disposed).
  void dispose() { }

  /// Whether this route is the top-most route on the navigator.
  bool get isCurrent {
    if (_navigator == null)
      return false;
    assert(_navigator._history.contains(this));
    return _navigator._history.last == this;
  }
Adam Barth's avatar
Adam Barth committed
65 66 67
}

class NamedRouteSettings {
68
  const NamedRouteSettings({ this.name, this.mostValuableKeys });
Adam Barth's avatar
Adam Barth committed
69 70
  final String name;
  final Set<Key> mostValuableKeys;
71 72 73 74 75 76 77 78 79 80

  String toString() {
    String result = '"$name"';
    if (mostValuableKeys != null && mostValuableKeys.isNotEmpty) {
      result += '; keys:';
      for (Key key in mostValuableKeys)
        result += ' $key';
    }
    return result;
  }
Adam Barth's avatar
Adam Barth committed
81 82 83
}

typedef Route RouteFactory(NamedRouteSettings settings);
Hixie's avatar
Hixie committed
84
typedef void NavigatorTransactionCallback(NavigatorTransaction transaction);
85

86
class NavigatorObserver {
87
  /// The navigator that the observer is observing, if any.
88
  NavigatorState get navigator => _navigator;
89
  NavigatorState _navigator;
90 91
  void didPush(Route route, Route previousRoute) { }
  void didPop(Route route, Route previousRoute) { }
92 93
}

94 95 96
class Navigator extends StatefulComponent {
  Navigator({
    Key key,
97
    this.initialRoute,
Adam Barth's avatar
Adam Barth committed
98
    this.onGenerateRoute,
99 100
    this.onUnknownRoute,
    this.observer
101
  }) : super(key: key) {
Adam Barth's avatar
Adam Barth committed
102
    assert(onGenerateRoute != null);
103 104
  }

105
  final String initialRoute;
Adam Barth's avatar
Adam Barth committed
106 107
  final RouteFactory onGenerateRoute;
  final RouteFactory onUnknownRoute;
108
  final NavigatorObserver observer;
Adam Barth's avatar
Adam Barth committed
109 110

  static const String defaultRouteName = '/';
111

Hixie's avatar
Hixie committed
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  static void pushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) {
    openTransaction(context, (NavigatorTransaction transaction) {
      transaction.pushNamed(routeName, mostValuableKeys: mostValuableKeys);
    });
  }

  static void push(BuildContext context, Route route, { Set<Key> mostValuableKeys }) {
    openTransaction(context, (NavigatorTransaction transaction) {
      transaction.push(route, mostValuableKeys: mostValuableKeys);
    });
  }

  static bool pop(BuildContext context, [ dynamic result ]) {
    bool returnValue;
    openTransaction(context, (NavigatorTransaction transaction) {
      returnValue = transaction.pop(result);
    });
    return returnValue;
  }

  static void popAndPushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) {
    openTransaction(context, (NavigatorTransaction transaction) {
      transaction.pop();
      transaction.pushNamed(routeName, mostValuableKeys: mostValuableKeys);
    });
  }

  static void openTransaction(BuildContext context, NavigatorTransactionCallback callback) {
    NavigatorState navigator = context.ancestorStateOfType(NavigatorState);
    navigator.openTransaction(callback);
  }
Adam Barth's avatar
Adam Barth committed
143

144
  NavigatorState createState() => new NavigatorState();
145 146
}

147
class NavigatorState extends State<Navigator> {
Adam Barth's avatar
Adam Barth committed
148
  final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
149
  final List<Route> _history = new List<Route>();
150

151 152
  void initState() {
    super.initState();
153 154
    assert(config.observer == null || config.observer.navigator == null);
    config.observer?._navigator = this;
Hixie's avatar
Hixie committed
155
    _push(config.onGenerateRoute(new NamedRouteSettings(
156 157
      name: config.initialRoute ?? Navigator.defaultRouteName
    )));
158 159
  }

160 161 162 163 164 165 166 167 168
  void didUpdateConfig(Navigator oldConfig) {
    if (oldConfig.observer != config.observer) {
      oldConfig.observer?._navigator = null;
      assert(config.observer == null || config.observer.navigator == null);
      config.observer?._navigator = this;
    }
  }

  void dispose() {
169 170
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
171
    config.observer?._navigator = null;
172 173 174 175
    for (Route route in _history) {
      route.dispose();
      route._navigator = null;
    }
176
    super.dispose();
177
    assert(() { _debugLocked = false; return true; });
178 179
  }

Hixie's avatar
Hixie committed
180
  // Used by Routes and NavigatorObservers
Adam Barth's avatar
Adam Barth committed
181
  OverlayState get overlay => _overlayKey.currentState;
182

Hixie's avatar
Hixie committed
183
  OverlayEntry get _currentOverlayEntry {
184
    for (Route route in _history.reversed) {
Adam Barth's avatar
Adam Barth committed
185 186
      if (route.overlayEntries.isNotEmpty)
        return route.overlayEntries.last;
187
    }
Adam Barth's avatar
Adam Barth committed
188
    return null;
189 190
  }

191 192
  bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends

Hixie's avatar
Hixie committed
193
  void _pushNamed(String name, { Set<Key> mostValuableKeys }) {
194
    assert(!_debugLocked);
195
    assert(name != null);
Adam Barth's avatar
Adam Barth committed
196 197 198
    NamedRouteSettings settings = new NamedRouteSettings(
      name: name,
      mostValuableKeys: mostValuableKeys
Hixie's avatar
Hixie committed
199
    );
Hixie's avatar
Hixie committed
200
    _push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
201 202
  }

Hixie's avatar
Hixie committed
203
  void _push(Route route, { Set<Key> mostValuableKeys }) {
204 205 206 207
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
    assert(route != null);
    assert(route._navigator == null);
Hixie's avatar
Hixie committed
208
    setState(() {
209
      Route oldRoute = _history.isNotEmpty ? _history.last : null;
210
      route._navigator = this;
Hixie's avatar
Hixie committed
211
      route.install(_currentOverlayEntry);
212
      _history.add(route);
213 214 215 216
      route.didPush();
      if (oldRoute != null)
        oldRoute.didPushNext(route);
      config.observer?.didPush(route, oldRoute);
Hixie's avatar
Hixie committed
217
    });
218
    assert(() { _debugLocked = false; return true; });
Hixie's avatar
Hixie committed
219 220
  }

Hixie's avatar
Hixie committed
221
  void _replace({ Route oldRoute, Route newRoute }) {
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    assert(!_debugLocked);
    assert(oldRoute != null);
    assert(newRoute != null);
    if (oldRoute == newRoute)
      return;
    assert(() { _debugLocked = true; return true; });
    assert(oldRoute._navigator == this);
    assert(newRoute._navigator == null);
    assert(oldRoute.overlayEntries.isNotEmpty);
    assert(newRoute.overlayEntries.isEmpty);
    assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last));
    setState(() {
      int index = _history.indexOf(oldRoute);
      assert(index >= 0);
      newRoute._navigator = this;
Hixie's avatar
Hixie committed
237
      newRoute.install(oldRoute.overlayEntries.last);
238 239 240 241 242 243 244 245 246 247
      _history[index] = newRoute;
      newRoute.didReplace(oldRoute);
      if (index > 0)
        _history[index - 1].didReplaceNext(oldRoute, newRoute);
      oldRoute.dispose();
      oldRoute._navigator = null;
    });
    assert(() { _debugLocked = false; return true; });
  }

Hixie's avatar
Hixie committed
248
  void _replaceRouteBefore({ Route anchorRoute, Route newRoute }) {
249 250 251
    assert(anchorRoute != null);
    assert(anchorRoute._navigator == this);
    assert(_history.indexOf(anchorRoute) > 0);
Hixie's avatar
Hixie committed
252
    _replace(oldRoute: _history[_history.indexOf(anchorRoute)-1], newRoute: newRoute);
253 254
  }
 
Hixie's avatar
Hixie committed
255
  void _removeRouteBefore(Route anchorRoute) {
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
    assert(anchorRoute._navigator == this);
    int index = _history.indexOf(anchorRoute) - 1;
    assert(index >= 0);
    Route targetRoute = _history[index];
    assert(targetRoute._navigator == this);
    assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last));
    setState(() {
      _history.removeAt(index);
      targetRoute.dispose();
      targetRoute._navigator = null;
    });
    assert(() { _debugLocked = false; return true; });
  }

Hixie's avatar
Hixie committed
272
  bool _pop([dynamic result]) {
273 274
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
Hixie's avatar
Hixie committed
275 276 277 278 279
    Route route = _history.last;
    assert(route._navigator == this);
    if (route.didPop(result)) {
      if (_history.length > 1) {
        setState(() {
280 281 282
          // We use setState to guarantee that we'll rebuild, since the routes
          // can't do that for themselves, even if they have changed their own
          // state (e.g. ModalScope.isCurrent).
Hixie's avatar
Hixie committed
283
          _history.removeLast();
284 285
          _history.last.didPopNext(route);
          config.observer?.didPop(route, _history.last);
Hixie's avatar
Hixie committed
286 287 288 289 290 291 292
          route._navigator = null;
        });
      } else {
        assert(() { _debugLocked = false; return true; });
        return false;
      }
    }
293
    assert(() { _debugLocked = false; return true; });
Hixie's avatar
Hixie committed
294
    return true;
Hixie's avatar
Hixie committed
295 296
  }

Hixie's avatar
Hixie committed
297
  void _popUntil(Route targetRoute) {
298 299
    assert(_history.contains(targetRoute));
    while (!targetRoute.isCurrent)
Hixie's avatar
Hixie committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
      _pop();
  }

  bool _hadTransaction = true;

  bool openTransaction(NavigatorTransactionCallback callback) {
    assert(callback != null);
    if (_hadTransaction)
      return false;
    _hadTransaction = true;
    NavigatorTransaction transaction = new NavigatorTransaction._(this);
    setState(() {
      callback(transaction);
    });
    assert(() { transaction._debugClose(); return true; });
    return true;
316 317
  }

Adam Barth's avatar
Adam Barth committed
318
  Widget build(BuildContext context) {
319
    assert(!_debugLocked);
320
    assert(_history.isNotEmpty);
Hixie's avatar
Hixie committed
321
    _hadTransaction = false;
Adam Barth's avatar
Adam Barth committed
322 323
    return new Overlay(
      key: _overlayKey,
324
      initialEntries: _history.first.overlayEntries
325 326 327
    );
  }
}
Hixie's avatar
Hixie committed
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

class NavigatorTransaction {
  NavigatorTransaction._(this._navigator) {
    assert(_navigator != null);
  }
  NavigatorState _navigator;
  bool _debugOpen = true;
 
  /// Invokes the Navigator's onGenerateRoute callback to create a route with
  /// the given name, then calls [push()] with that route.
  void pushNamed(String name, { Set<Key> mostValuableKeys }) {
    assert(_debugOpen);
    _navigator._pushNamed(name, mostValuableKeys: mostValuableKeys);
  }

  /// Adds the given route to the Navigator's history, and transitions to it.
  /// The route will have didPush() called on it; the previous route, if any,
  /// will have didPushNext() called on it; and the Navigator observer, if any,
  /// will have didPush() called on it.
  void push(Route route, { Set<Key> mostValuableKeys }) {
    assert(_debugOpen);
    _navigator._push(route, mostValuableKeys: mostValuableKeys);
  }

  /// Replaces one given route with another, but does not call didPush/didPop.
  /// Instead, this calls install() on the new route, then didReplace() on the
  /// new route passing the old route, then dispose() on the old route. The
  /// navigator is not informed of the replacement.
  ///
  /// The old route must have overlay entries, otherwise we won't know where to
  /// insert the entries of the new route. The old route must not be currently
  /// visible (i.e. a later route have overlay entries that are currently
  /// opaque), otherwise the replacement would have a jarring effect.
  ///
  /// It is safe to call this redundantly (replacing a route with itself). Such
  /// calls are ignored.
  void replace({ Route oldRoute, Route newRoute }) {
    assert(_debugOpen);
    _navigator._replace(oldRoute: oldRoute, newRoute: newRoute);
  }

  /// Like replace(), but affects the route before the given anchorRoute rather
  /// than the anchorRoute itself.
  ///
  /// If newRoute is already the route before anchorRoute, then the call is
  /// ignored.
  ///
  /// The conditions described for [replace()] apply; for instance, the route
  /// before anchorRoute must have overlay entries.
  void replaceRouteBefore({ Route anchorRoute, Route newRoute }) {
    assert(_debugOpen);
    _navigator._replaceRouteBefore(anchorRoute: anchorRoute, newRoute: newRoute);
  }

  /// Removes the route prior to the given anchorRoute without notifying
  /// neighbouring routes or the navigator observer, if any.
  void removeRouteBefore(Route anchorRoute) {
    assert(_debugOpen);
    _navigator._removeRouteBefore(anchorRoute);
  }

  /// Tries to removes the current route, calling its didPop() method. If that
  /// method returns false, then nothing else happens. Otherwise, the observer
  /// (if any) is notified using its didPop() method, and the previous route is
  /// notified using [Route.didPopNext].
  ///
  /// The type of the result argument, if provided, must match the type argument
  /// of the class of the current route. (In practice, this is usually
  /// "dynamic".)
  ///
  /// Returns true if a route was popped; returns false if there are no further
  /// previous routes.
  bool pop([dynamic result]) {
    assert(_debugOpen);
    return _navigator._pop(result);
  }
 
  /// Calls pop() repeatedly until the given route is the current route.
  /// If it is already the current route, nothing happens.
  void popUntil(Route targetRoute) {
    assert(_debugOpen);
    _navigator._popUntil(targetRoute);
  }

  void _debugClose() {
    assert(_debugOpen);
    _debugOpen = false;
  }
}