navigator.dart 5.78 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

Adam Barth's avatar
Adam Barth committed
8 9
abstract class Route {
  List<OverlayEntry> get overlayEntries;
10 11 12 13 14 15 16 17 18 19 20 21 22 23
  void didPush(OverlayState overlay, OverlayEntry insertionPoint) { }
  void didPop(dynamic result) { }

  /// The given route has been pushed onto the navigator after this route.
  /// Return true if the route before this one should be notified also. The
  /// first route to return false will be the one passed to the
  /// NavigatorObserver's didPushModal() as the previousRoute.
  bool willPushNext(Route nextRoute) => false;

  /// The given route, which came after this one, has been popped off the
  /// navigator. Return true if the route before this one should be notified
  /// also. The first route to return false will be the one passed to the
  /// NavigatorObserver's didPushModal() as the previousRoute.
  bool didPopNext(Route nextRoute) => false;
Adam Barth's avatar
Adam Barth committed
24 25 26
}

class NamedRouteSettings {
27
  const NamedRouteSettings({ this.name, this.mostValuableKeys });
Adam Barth's avatar
Adam Barth committed
28 29 30 31 32
  final String name;
  final Set<Key> mostValuableKeys;
}

typedef Route RouteFactory(NamedRouteSettings settings);
33

34 35 36
class NavigatorObserver {
  NavigatorState _navigator;
  NavigatorState get navigator => _navigator;
37 38
  void didPushModal(Route route, Route previousRoute) { }
  void didPopModal(Route route, Route previousRoute) { }
39 40
}

41 42 43
class Navigator extends StatefulComponent {
  Navigator({
    Key key,
Adam Barth's avatar
Adam Barth committed
44
    this.onGenerateRoute,
45 46
    this.onUnknownRoute,
    this.observer
47
  }) : super(key: key) {
Adam Barth's avatar
Adam Barth committed
48
    assert(onGenerateRoute != null);
49 50
  }

Adam Barth's avatar
Adam Barth committed
51 52
  final RouteFactory onGenerateRoute;
  final RouteFactory onUnknownRoute;
53
  final NavigatorObserver observer;
Adam Barth's avatar
Adam Barth committed
54 55

  static const String defaultRouteName = '/';
56

Hixie's avatar
Hixie committed
57
  static NavigatorState of(BuildContext context) => context.ancestorStateOfType(NavigatorState);
Adam Barth's avatar
Adam Barth committed
58

59
  NavigatorState createState() => new NavigatorState();
60 61
}

62
class NavigatorState extends State<Navigator> {
Adam Barth's avatar
Adam Barth committed
63 64 65
  final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
  final List<Route> _ephemeral = new List<Route>();
  final List<Route> _modal = new List<Route>();
66

67 68
  void initState() {
    super.initState();
69 70
    assert(config.observer == null || config.observer.navigator == null);
    config.observer?._navigator = this;
Adam Barth's avatar
Adam Barth committed
71
    push(config.onGenerateRoute(new NamedRouteSettings(name: Navigator.defaultRouteName)));
72 73
  }

74 75 76 77 78 79 80 81 82 83 84 85 86
  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() {
    config.observer?._navigator = null;
    super.dispose();
  }

Adam Barth's avatar
Adam Barth committed
87 88
  bool get hasPreviousRoute => _modal.length > 1;
  OverlayState get overlay => _overlayKey.currentState;
89

Adam Barth's avatar
Adam Barth committed
90 91 92 93
  OverlayEntry get _currentOverlay {
    for (Route route in _ephemeral.reversed) {
      if (route.overlayEntries.isNotEmpty)
        return route.overlayEntries.last;
94
    }
Adam Barth's avatar
Adam Barth committed
95 96 97
    for (Route route in _modal.reversed) {
      if (route.overlayEntries.isNotEmpty)
        return route.overlayEntries.last;
98
    }
Adam Barth's avatar
Adam Barth committed
99
    return null;
100 101
  }

Adam Barth's avatar
Adam Barth committed
102
  void pushNamed(String name, { Set<Key> mostValuableKeys }) {
103
    assert(name != null);
Adam Barth's avatar
Adam Barth committed
104 105 106
    NamedRouteSettings settings = new NamedRouteSettings(
      name: name,
      mostValuableKeys: mostValuableKeys
Hixie's avatar
Hixie committed
107
    );
Adam Barth's avatar
Adam Barth committed
108
    push(config.onGenerateRoute(settings) ?? config.onUnknownRoute(settings));
109 110
  }

111
  void push(Route route, { Set<Key> mostValuableKeys }) {
Hixie's avatar
Hixie committed
112 113 114 115 116 117 118 119 120
    setState(() {
      _popAllEphemeralRoutes();
      int index = _modal.length-1;
      while (index >= 0 && _modal[index].willPushNext(route))
        index -= 1;
      route.didPush(overlay, _currentOverlay);
      config.observer?.didPushModal(route, index >= 0 ? _modal[index] : null);
      _modal.add(route);
    });
Hixie's avatar
Hixie committed
121 122
  }

Adam Barth's avatar
Adam Barth committed
123 124 125
  void pushEphemeral(Route route) {
    route.didPush(overlay, _currentOverlay);
    _ephemeral.add(route);
126 127
  }

Adam Barth's avatar
Adam Barth committed
128 129 130 131 132 133
  void _popAllEphemeralRoutes() {
    List<Route> localEphemeral = new List<Route>.from(_ephemeral);
    _ephemeral.clear();
    for (Route route in localEphemeral)
      route.didPop(null);
    assert(_ephemeral.isEmpty);
134
  }
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
  /// Pops the given route, if it's the current route. If it's not the current
  /// route, removes it from the list of active routes without notifying any
  /// observers or adjacent routes.
  ///
  /// Do not use this for ModalRoutes, or indeed anything other than
  /// StateRoutes. Doing so would cause very odd results, e.g. ModalRoutes would
  /// get confused about who is current.
  void remove(Route route, [dynamic result]) {
    assert(_modal.contains(route));
    assert(route.overlayEntries.isEmpty);
    if (_modal.last == route) {
      pop(result);
    } else {
      setState(() {
        _modal.remove(route);
        route.didPop(result);
      });
    }
  }

  /// Removes the current route, notifying the observer (if any), and the
  /// previous routes (using [Route.didPopNext]).
Adam Barth's avatar
Adam Barth committed
158
  void pop([dynamic result]) {
Hixie's avatar
Hixie committed
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
    setState(() {
      // 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).
      if (_ephemeral.isNotEmpty) {
        _ephemeral.removeLast().didPop(result);
      } else {
        assert(_modal.length > 1);
        Route route = _modal.removeLast();
        route.didPop(result);
        int index = _modal.length-1;
        while (index >= 0 && _modal[index].didPopNext(route))
          index -= 1;
        config.observer?.didPopModal(route, index >= 0 ? _modal[index] : null);
      }
    });
Hixie's avatar
Hixie committed
175 176
  }

Adam Barth's avatar
Adam Barth committed
177 178 179 180
  Widget build(BuildContext context) {
    return new Overlay(
      key: _overlayKey,
      initialEntries: _modal.first.overlayEntries
181 182 183
    );
  }
}