blob: ad7558f7ecfc5c3d6c61f850710ff54d046c9133 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
part of engine;
const MethodCall _popRouteMethodCall = MethodCall('popRoute');
Map<String, bool> _originState = <String, bool>{'origin': true};
Map<String, bool> _flutterState = <String, bool>{'flutter': true};
/// The origin entry is the history entry that the Flutter app landed on. It's
/// created by the browser when the user navigates to the url of the app.
bool _isOriginEntry(dynamic state) {
return state is Map && state['origin'] == true;
}
/// The flutter entry is a history entry that we maintain on top of the origin
/// entry. It allows us to catch popstate events when the user hits the back
/// button.
bool _isFlutterEntry(dynamic state) {
return state is Map && state['flutter'] == true;
}
/// The [BrowserHistory] class is responsible for integrating Flutter Web apps
/// with the browser history so that the back button works as expected.
///
/// It does that by always keeping a single entry (conventionally called the
/// "flutter" entry) at the top of the browser history. That way, the browser's
/// back button always triggers a `popstate` event and never closes the app (we
/// close the app programmatically by calling [SystemNavigator.pop] when there
/// are no more app routes to be popped).
///
/// There should only be one global instance of this class.
class BrowserHistory {
LocationStrategy _locationStrategy;
ui.VoidCallback _unsubscribe;
/// Changing the location strategy will unsubscribe from the old strategy's
/// event listeners, and subscribe to the new one.
///
/// If the given [strategy] is the same as the existing one, nothing will
/// happen.
///
/// If the given strategy is null, it will render this [BrowserHistory]
/// instance inactive.
set locationStrategy(LocationStrategy strategy) {
if (strategy != _locationStrategy) {
_tearoffStrategy(_locationStrategy);
_locationStrategy = strategy;
_setupStrategy(_locationStrategy);
}
}
/// The path of the current location of the user's browser.
String get currentPath => _locationStrategy?.path ?? '/';
/// Update the url with the given [routeName].
void setRouteName(String routeName) {
if (_locationStrategy != null) {
_setupFlutterEntry(_locationStrategy, replace: true, path: routeName);
}
}
/// This method does the same thing as the browser back button.
Future<void> back() {
if (_locationStrategy != null) {
return _locationStrategy.back();
}
return Future<void>.value();
}
/// This method exits the app and goes to whatever website was active before.
Future<void> exit() {
if (_locationStrategy != null) {
_tearoffStrategy(_locationStrategy);
// After tearing off the location strategy, we should be on the "origin"
// entry. So we need to go back one more time to exit the app.
final Future<void> backFuture = _locationStrategy.back();
_locationStrategy = null;
return backFuture;
}
return Future<void>.value();
}
String _userProvidedRouteName;
void _popStateListener(covariant html.PopStateEvent event) {
if (_isOriginEntry(event.state)) {
// If we find ourselves in the origin entry, it means that the user
// clicked the back button.
// 1. Re-push the flutter entry to keep it always at the top of history.
_setupFlutterEntry(_locationStrategy);
// 2. Send a 'popRoute' platform message so the app can handle it accordingly.
if (window._onPlatformMessage != null) {
window.invokeOnPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(_popRouteMethodCall),
(_) {},
);
}
} else if (_isFlutterEntry(event.state)) {
// We get into this scenario when the user changes the url manually. It
// causes a new entry to be pushed on top of our "flutter" one. When this
// happens it first goes to the "else" section below where we capture the
// path into `_userProvidedRouteName` then trigger a history back which
// brings us here.
assert(_userProvidedRouteName != null);
final String newRouteName = _userProvidedRouteName;
_userProvidedRouteName = null;
// Send a 'pushRoute' platform message so the app handles it accordingly.
if (window._onPlatformMessage != null) {
window.invokeOnPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(
MethodCall('pushRoute', newRouteName),
),
(_) {},
);
}
} else {
// The user has pushed a new entry on top of our flutter entry. This could
// happen when the user modifies the hash part of the url directly, for
// example.
// 1. We first capture the user's desired path.
_userProvidedRouteName = currentPath;
// 2. Then we remove the new entry.
// This will take us back to our "flutter" entry and it causes a new
// popstate event that will be handled in the "else if" section above.
_locationStrategy.back();
}
}
/// This method should be called when the Origin Entry is active. It just
/// replaces the state of the entry so that we can recognize it later using
/// [_isOriginEntry] inside [_popStateListener].
void _setupOriginEntry(LocationStrategy strategy) {
assert(strategy != null);
strategy.replaceState(_originState, 'origin', '');
}
/// This method is used manipulate the Flutter Entry which is always the
/// active entry while the Flutter app is running.
void _setupFlutterEntry(
LocationStrategy strategy, {
bool replace = false,
String path,
}) {
assert(strategy != null);
path ??= currentPath;
if (replace) {
strategy.replaceState(_flutterState, 'flutter', path);
} else {
strategy.pushState(_flutterState, 'flutter', path);
}
}
void _setupStrategy(LocationStrategy strategy) {
if (strategy == null) {
return;
}
final String path = currentPath;
if (_isFlutterEntry(html.window.history.state)) {
// This could happen if the user, for example, refreshes the page. They
// will land directly on the "flutter" entry, so there's no need to setup
// the "origin" and "flutter" entries, we can safely assume they are
// already setup.
} else {
_setupOriginEntry(strategy);
_setupFlutterEntry(strategy, replace: false, path: path);
}
_unsubscribe = strategy.onPopState(_popStateListener);
}
void _tearoffStrategy(LocationStrategy strategy) {
if (strategy == null) {
return;
}
assert(_unsubscribe != null);
_unsubscribe();
_unsubscribe = null;
// Remove the "flutter" entry and go back to the "origin" entry so that the
// next location strategy can start from the right spot.
strategy.back();
}
}