diff --git a/lib/web_ui/lib/src/engine/navigation/history.dart b/lib/web_ui/lib/src/engine/navigation/history.dart index c5eec94e78f02..9c6de07e95edf 100644 --- a/lib/web_ui/lib/src/engine/navigation/history.dart +++ b/lib/web_ui/lib/src/engine/navigation/history.dart @@ -11,6 +11,20 @@ import '../services/message_codec.dart'; import '../services/message_codecs.dart'; import 'url_strategy.dart'; +/// Infers the history mode from the existing browser history state, then +/// creates the appropriate instance of [BrowserHistory] for it. +/// +/// If it can't infer, it creates a [MultiEntriesBrowserHistory] by default. +BrowserHistory createHistoryForExistingState(UrlStrategy? urlStrategy) { + if (urlStrategy != null) { + final Object? state = urlStrategy.getState(); + if (SingleEntryBrowserHistory._isOriginEntry(state) || SingleEntryBrowserHistory._isFlutterEntry(state)) { + return SingleEntryBrowserHistory(urlStrategy: urlStrategy); + } + } + return MultiEntriesBrowserHistory(urlStrategy: urlStrategy); +} + /// An abstract class that provides the API for [EngineWindow] to delegate its /// navigating events. /// @@ -263,14 +277,14 @@ class SingleEntryBrowserHistory extends BrowserHistory { /// 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(Object? state) { + static bool _isOriginEntry(Object? state) { return state is Map && state[_kOriginTag] == 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(Object? state) { + static bool _isFlutterEntry(Object? state) { return state is Map && state[_kFlutterTag] == true; } diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 419aa8070e47a..17c1271513529 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -13,7 +13,6 @@ import 'package:js/js.dart'; import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; -import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; import 'navigation/history.dart'; import 'navigation/js_url_strategy.dart'; @@ -50,12 +49,8 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { engineDispatcher.windows[_windowId] = this; engineDispatcher.windowConfigurations[_windowId] = const ui.ViewConfiguration(); if (_isUrlStrategySet) { - _browserHistory = - MultiEntriesBrowserHistory(urlStrategy: _customUrlStrategy); + _browserHistory = createHistoryForExistingState(_customUrlStrategy); } - registerHotRestartListener(() { - window.resetHistory(); - }); } final Object _windowId; @@ -67,7 +62,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { /// button, etc. BrowserHistory get browserHistory { return _browserHistory ??= - MultiEntriesBrowserHistory(urlStrategy: _urlStrategyForInitialization); + createHistoryForExistingState(_urlStrategyForInitialization); } UrlStrategy? get _urlStrategyForInitialization { @@ -82,32 +77,50 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { _browserHistory; // Must be either SingleEntryBrowserHistory or MultiEntriesBrowserHistory. Future _useSingleEntryBrowserHistory() async { + // Recreate the browser history mode that's appropriate for the existing + // history state. + // + // If it happens to be a single-entry one, then there's nothing further to do. + // + // But if it's a multi-entry one, it will be torn down below and replaced + // with a single-entry history. + // + // See: https://github.com/flutter/flutter/issues/79241 + _browserHistory ??= + createHistoryForExistingState(_urlStrategyForInitialization); + if (_browserHistory is SingleEntryBrowserHistory) { return; } - final UrlStrategy? strategy; - if (_browserHistory == null) { - strategy = _urlStrategyForInitialization; - } else { - strategy = _browserHistory?.urlStrategy; - await _browserHistory?.tearDown(); - } + // At this point, we know that `_browserHistory` is a non-null + // `MultiEntriesBrowserHistory` instance. + final UrlStrategy? strategy = _browserHistory?.urlStrategy; + await _browserHistory?.tearDown(); _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); } Future _useMultiEntryBrowserHistory() async { + // Recreate the browser history mode that's appropriate for the existing + // history state. + // + // If it happens to be a multi-entry one, then there's nothing further to do. + // + // But if it's a single-entry one, it will be torn down below and replaced + // with a multi-entry history. + // + // See: https://github.com/flutter/flutter/issues/79241 + _browserHistory ??= + createHistoryForExistingState(_urlStrategyForInitialization); + if (_browserHistory is MultiEntriesBrowserHistory) { return; } - final UrlStrategy? strategy; - if (_browserHistory == null) { - strategy = _urlStrategyForInitialization; - } else { - strategy = _browserHistory?.urlStrategy; - await _browserHistory?.tearDown(); - } + // At this point, we know that `_browserHistory` is a non-null + // `SingleEntryBrowserHistory` instance. + final UrlStrategy? strategy = _browserHistory?.urlStrategy; + await _browserHistory?.tearDown(); _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); } diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index 08b2aed6ad856..7f87324b4091d 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -39,6 +39,50 @@ void main() { } void testMain() { + test('createHistoryForExistingState', () { + TestUrlStrategy strategy; + BrowserHistory history; + + // No url strategy. + history = createHistoryForExistingState(null); + expect(history, isA()); + expect(history.urlStrategy, isNull); + + // Random history state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'foo': 123}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Multi-entry history state. + final Map state = { + 'serialCount': 1, + 'state': {'foo': 123}, + }; + strategy = TestUrlStrategy.fromEntry(TestHistoryEntry(state, null, '/')); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Single-entry history "origin" state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'origin': true}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + + // Single-entry history "flutter" state. + strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry({'flutter': true}, null, '/'), + ); + history = createHistoryForExistingState(strategy); + expect(history, isA()); + expect(history.urlStrategy, strategy); + }); + group('$SingleEntryBrowserHistory', () { final PlatformMessagesSpy spy = PlatformMessagesSpy();