diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 63aa5cc65f1bd..2da5356ff13d5 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -78,7 +78,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _addFontSizeObserver(); _addLocaleChangedListener(); registerHotRestartListener(dispose); - AppLifecycleState.instance.addListener(_setAppLifecycleState); + _appLifecycleState.addListener(_setAppLifecycleState); _viewFocusBinding.init(); domDocument.body?.prepend(accessibilityPlaceholder); _onViewDisposedListener = viewManager.onViewDisposed.listen((_) { @@ -122,7 +122,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _disconnectFontSizeObserver(); _removeLocaleChangedListener(); HighContrastSupport.instance.removeListener(_updateHighContrast); - AppLifecycleState.instance.removeListener(_setAppLifecycleState); + _appLifecycleState.removeListener(_setAppLifecycleState); _viewFocusBinding.dispose(); accessibilityPlaceholder.remove(); _onViewDisposedListener.cancel(); @@ -155,6 +155,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { late final FlutterViewManager viewManager = FlutterViewManager(this); + late final AppLifecycleState _appLifecycleState = + AppLifecycleState.create(viewManager); + /// The current list of windows. @override Iterable get views => viewManager.views; diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart b/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart index 138fcccab4dbf..27db846163035 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; @@ -12,7 +14,9 @@ typedef AppLifecycleStateListener = void Function(ui.AppLifecycleState state); /// Determines the [ui.AppLifecycleState]. abstract class AppLifecycleState { - static final AppLifecycleState instance = _BrowserAppLifecycleState(); + static AppLifecycleState create(FlutterViewManager viewManager) { + return _BrowserAppLifecycleState(viewManager); + } ui.AppLifecycleState get appLifecycleState => _appLifecycleState; ui.AppLifecycleState _appLifecycleState = ui.AppLifecycleState.resumed; @@ -56,28 +60,36 @@ abstract class AppLifecycleState { /// browser events. /// /// This class listens to: -/// - 'beforeunload' on [DomWindow] to detect detachment, /// - 'visibilitychange' on [DomHTMLDocument] to observe visibility changes, /// - 'focus' and 'blur' on [DomWindow] to track application focus shifts. class _BrowserAppLifecycleState extends AppLifecycleState { + _BrowserAppLifecycleState(this._viewManager); + + final FlutterViewManager _viewManager; + final List> _subscriptions = >[]; + @override void activate() { domWindow.addEventListener('focus', _focusListener); domWindow.addEventListener('blur', _blurListener); - // TODO(web): Register 'beforeunload' only if lifecycle listeners exist, to improve efficiency: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes - domWindow.addEventListener('beforeunload', _beforeUnloadListener); domDocument.addEventListener('visibilitychange', _visibilityChangeListener); + _subscriptions + ..add(_viewManager.onViewCreated.listen(_onViewCountChanged)) + ..add(_viewManager.onViewDisposed.listen(_onViewCountChanged)); } @override void deactivate() { domWindow.removeEventListener('focus', _focusListener); domWindow.removeEventListener('blur', _blurListener); - domWindow.removeEventListener('beforeunload', _beforeUnloadListener); domDocument.removeEventListener( 'visibilitychange', _visibilityChangeListener, ); + for (final StreamSubscription subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); } late final DomEventListener _focusListener = @@ -90,11 +102,6 @@ class _BrowserAppLifecycleState extends AppLifecycleState { onAppLifecycleStateChange(ui.AppLifecycleState.inactive); }); - late final DomEventListener _beforeUnloadListener = - createDomEventListener((DomEvent event) { - onAppLifecycleStateChange(ui.AppLifecycleState.detached); - }); - late final DomEventListener _visibilityChangeListener = createDomEventListener((DomEvent event) { if (domDocument.visibilityState == 'visible') { @@ -103,4 +110,12 @@ class _BrowserAppLifecycleState extends AppLifecycleState { onAppLifecycleStateChange(ui.AppLifecycleState.hidden); } }); + + void _onViewCountChanged(_) { + if (_viewManager.views.isEmpty) { + onAppLifecycleStateChange(ui.AppLifecycleState.detached); + } else { + onAppLifecycleStateChange(ui.AppLifecycleState.resumed); + } + } } diff --git a/lib/web_ui/test/engine/platform_dispatcher/app_lifecycle_state_test.dart b/lib/web_ui/test/engine/platform_dispatcher/app_lifecycle_state_test.dart new file mode 100644 index 0000000000000..6e02aa6192c1c --- /dev/null +++ b/lib/web_ui/test/engine/platform_dispatcher/app_lifecycle_state_test.dart @@ -0,0 +1,54 @@ +// 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. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group(AppLifecycleState, () { + test('listens to changes in view manager', () { + final FlutterViewManager viewManager = FlutterViewManager(EnginePlatformDispatcher.instance); + final AppLifecycleState state = AppLifecycleState.create(viewManager); + + ui.AppLifecycleState? currentState; + void listener(ui.AppLifecycleState newState) { + currentState = newState; + } + + state.addListener(listener); + + final view1 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement()); + viewManager.registerView(view1); + expect(currentState, ui.AppLifecycleState.resumed); + currentState = null; + + final view2 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement()); + viewManager.registerView(view2); + // The listener should not be called again. The view manager is still not empty. + expect(currentState, isNull); + + viewManager.disposeAndUnregisterView(view1.viewId); + // The listener should not be called again. The view manager is still not empty. + expect(currentState, isNull); + + viewManager.disposeAndUnregisterView(view2.viewId); + expect(currentState, ui.AppLifecycleState.detached); + currentState = null; + + final view3 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement()); + viewManager.registerView(view3); + // The state should go back to `resumed` after a new view is registered. + expect(currentState, ui.AppLifecycleState.resumed); + + viewManager.dispose(); + state.removeListener(listener); + }); + }); +}