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