• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7const MethodCall _popRouteMethodCall = MethodCall('popRoute');
8
9Map<String, bool> _originState = <String, bool>{'origin': true};
10Map<String, bool> _flutterState = <String, bool>{'flutter': true};
11
12/// The origin entry is the history entry that the Flutter app landed on. It's
13/// created by the browser when the user navigates to the url of the app.
14bool _isOriginEntry(dynamic state) {
15  return state is Map && state['origin'] == true;
16}
17
18/// The flutter entry is a history entry that we maintain on top of the origin
19/// entry. It allows us to catch popstate events when the user hits the back
20/// button.
21bool _isFlutterEntry(dynamic state) {
22  return state is Map && state['flutter'] == true;
23}
24
25/// The [BrowserHistory] class is responsible for integrating Flutter Web apps
26/// with the browser history so that the back button works as expected.
27///
28/// It does that by always keeping a single entry (conventionally called the
29/// "flutter" entry) at the top of the browser history. That way, the browser's
30/// back button always triggers a `popstate` event and never closes the app (we
31/// close the app programmatically by calling [SystemNavigator.pop] when there
32/// are no more app routes to be popped).
33///
34/// There should only be one global instance of this class.
35class BrowserHistory {
36  LocationStrategy _locationStrategy;
37  ui.VoidCallback _unsubscribe;
38
39  /// Changing the location strategy will unsubscribe from the old strategy's
40  /// event listeners, and subscribe to the new one.
41  ///
42  /// If the given [strategy] is the same as the existing one, nothing will
43  /// happen.
44  ///
45  /// If the given strategy is null, it will render this [BrowserHistory]
46  /// instance inactive.
47  set locationStrategy(LocationStrategy strategy) {
48    if (strategy != _locationStrategy) {
49      _tearoffStrategy(_locationStrategy);
50      _locationStrategy = strategy;
51      _setupStrategy(_locationStrategy);
52    }
53  }
54
55  /// The path of the current location of the user's browser.
56  String get currentPath => _locationStrategy?.path ?? '/';
57
58  /// Update the url with the given [routeName].
59  void setRouteName(String routeName) {
60    if (_locationStrategy != null) {
61      _setupFlutterEntry(_locationStrategy, replace: true, path: routeName);
62    }
63  }
64
65  /// This method does the same thing as the browser back button.
66  Future<void> back() {
67    if (_locationStrategy != null) {
68      return _locationStrategy.back();
69    }
70    return Future<void>.value();
71  }
72
73  /// This method exits the app and goes to whatever website was active before.
74  Future<void> exit() {
75    if (_locationStrategy != null) {
76      _tearoffStrategy(_locationStrategy);
77      // After tearing off the location strategy, we should be on the "origin"
78      // entry. So we need to go back one more time to exit the app.
79      final Future<void> backFuture = _locationStrategy.back();
80      _locationStrategy = null;
81      return backFuture;
82    }
83    return Future<void>.value();
84  }
85
86  String _userProvidedRouteName;
87  void _popStateListener(covariant html.PopStateEvent event) {
88    if (_isOriginEntry(event.state)) {
89      // If we find ourselves in the origin entry, it means that the user
90      // clicked the back button.
91
92      // 1. Re-push the flutter entry to keep it always at the top of history.
93      _setupFlutterEntry(_locationStrategy);
94
95      // 2. Send a 'popRoute' platform message so the app can handle it accordingly.
96      ui.window.onPlatformMessage(
97        'flutter/navigation',
98        const JSONMethodCodec().encodeMethodCall(_popRouteMethodCall),
99        (_) {},
100      );
101    } else if (_isFlutterEntry(event.state)) {
102      // We get into this scenario when the user changes the url manually. It
103      // causes a new entry to be pushed on top of our "flutter" one. When this
104      // happens it first goes to the "else" section below where we capture the
105      // path into `_userProvidedRouteName` then trigger a history back which
106      // brings us here.
107      assert(_userProvidedRouteName != null);
108
109      final String newRouteName = _userProvidedRouteName;
110      _userProvidedRouteName = null;
111
112      // Send a 'pushRoute' platform message so the app handles it accordingly.
113      ui.window.onPlatformMessage(
114        'flutter/navigation',
115        const JSONMethodCodec().encodeMethodCall(
116          MethodCall('pushRoute', newRouteName),
117        ),
118        (_) {},
119      );
120    } else {
121      // The user has pushed a new entry on top of our flutter entry. This could
122      // happen when the user modifies the hash part of the url directly, for
123      // example.
124
125      // 1. We first capture the user's desired path.
126      _userProvidedRouteName = currentPath;
127
128      // 2. Then we remove the new entry.
129      // This will take us back to our "flutter" entry and it causes a new
130      // popstate event that will be handled in the "else if" section above.
131      _locationStrategy.back();
132    }
133  }
134
135  /// This method should be called when the Origin Entry is active. It just
136  /// replaces the state of the entry so that we can recognize it later using
137  /// [_isOriginEntry] inside [_popStateListener].
138  void _setupOriginEntry(LocationStrategy strategy) {
139    assert(strategy != null);
140    strategy.replaceState(_originState, 'origin', '');
141  }
142
143  /// This method is used manipulate the Flutter Entry which is always the
144  /// active entry while the Flutter app is running.
145  void _setupFlutterEntry(
146    LocationStrategy strategy, {
147    bool replace = false,
148    String path,
149  }) {
150    assert(strategy != null);
151    path ??= currentPath;
152    if (replace) {
153      strategy.replaceState(_flutterState, 'flutter', path);
154    } else {
155      strategy.pushState(_flutterState, 'flutter', path);
156    }
157  }
158
159  void _setupStrategy(LocationStrategy strategy) {
160    if (strategy == null) {
161      return;
162    }
163
164    final String path = currentPath;
165    if (_isFlutterEntry(html.window.history.state)) {
166      // This could happen if the user, for example, refreshes the page. They
167      // will land directly on the "flutter" entry, so there's no need to setup
168      // the "origin" and "flutter" entries, we can safely assume they are
169      // already setup.
170    } else {
171      _setupOriginEntry(strategy);
172      _setupFlutterEntry(strategy, replace: false, path: path);
173    }
174    _unsubscribe = strategy.onPopState(_popStateListener);
175  }
176
177  void _tearoffStrategy(LocationStrategy strategy) {
178    if (strategy == null) {
179      return;
180    }
181
182    assert(_unsubscribe != null);
183    _unsubscribe();
184    _unsubscribe = null;
185
186    // Remove the "flutter" entry and go back to the "origin" entry so that the
187    // next location strategy can start from the right spot.
188    strategy.back();
189  }
190}
191