Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6313b1e

Browse files
authored
[web] Implement AppLifecycleState.detached as documented (#53506)
Currently, we are transitioning to the `AppLifecycleState.detached` incorrectly. This is causing the framework to stop pumping frames when the app is still active and visible. This PR re-implements the transition to `AppLifecycleState.detached` as documented [here](https://api.flutter.dev/flutter/dart-ui/AppLifecycleState.html#detached) (based on whether the app has any views or not). Fixes flutter/flutter#150636 Fixes flutter/flutter#149417
1 parent fbd9205 commit 6313b1e

File tree

3 files changed

+84
-12
lines changed

3 files changed

+84
-12
lines changed

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
7878
_addFontSizeObserver();
7979
_addLocaleChangedListener();
8080
registerHotRestartListener(dispose);
81-
AppLifecycleState.instance.addListener(_setAppLifecycleState);
81+
_appLifecycleState.addListener(_setAppLifecycleState);
8282
_viewFocusBinding.init();
8383
domDocument.body?.prepend(accessibilityPlaceholder);
8484
_onViewDisposedListener = viewManager.onViewDisposed.listen((_) {
@@ -122,7 +122,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
122122
_disconnectFontSizeObserver();
123123
_removeLocaleChangedListener();
124124
HighContrastSupport.instance.removeListener(_updateHighContrast);
125-
AppLifecycleState.instance.removeListener(_setAppLifecycleState);
125+
_appLifecycleState.removeListener(_setAppLifecycleState);
126126
_viewFocusBinding.dispose();
127127
accessibilityPlaceholder.remove();
128128
_onViewDisposedListener.cancel();
@@ -155,6 +155,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
155155

156156
late final FlutterViewManager viewManager = FlutterViewManager(this);
157157

158+
late final AppLifecycleState _appLifecycleState =
159+
AppLifecycleState.create(viewManager);
160+
158161
/// The current list of windows.
159162
@override
160163
Iterable<EngineFlutterView> get views => viewManager.views;

lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:meta/meta.dart';
68
import 'package:ui/ui.dart' as ui;
79

@@ -12,7 +14,9 @@ typedef AppLifecycleStateListener = void Function(ui.AppLifecycleState state);
1214

1315
/// Determines the [ui.AppLifecycleState].
1416
abstract class AppLifecycleState {
15-
static final AppLifecycleState instance = _BrowserAppLifecycleState();
17+
static AppLifecycleState create(FlutterViewManager viewManager) {
18+
return _BrowserAppLifecycleState(viewManager);
19+
}
1620

1721
ui.AppLifecycleState get appLifecycleState => _appLifecycleState;
1822
ui.AppLifecycleState _appLifecycleState = ui.AppLifecycleState.resumed;
@@ -56,28 +60,36 @@ abstract class AppLifecycleState {
5660
/// browser events.
5761
///
5862
/// This class listens to:
59-
/// - 'beforeunload' on [DomWindow] to detect detachment,
6063
/// - 'visibilitychange' on [DomHTMLDocument] to observe visibility changes,
6164
/// - 'focus' and 'blur' on [DomWindow] to track application focus shifts.
6265
class _BrowserAppLifecycleState extends AppLifecycleState {
66+
_BrowserAppLifecycleState(this._viewManager);
67+
68+
final FlutterViewManager _viewManager;
69+
final List<StreamSubscription<void>> _subscriptions = <StreamSubscription<void>>[];
70+
6371
@override
6472
void activate() {
6573
domWindow.addEventListener('focus', _focusListener);
6674
domWindow.addEventListener('blur', _blurListener);
67-
// 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
68-
domWindow.addEventListener('beforeunload', _beforeUnloadListener);
6975
domDocument.addEventListener('visibilitychange', _visibilityChangeListener);
76+
_subscriptions
77+
..add(_viewManager.onViewCreated.listen(_onViewCountChanged))
78+
..add(_viewManager.onViewDisposed.listen(_onViewCountChanged));
7079
}
7180

7281
@override
7382
void deactivate() {
7483
domWindow.removeEventListener('focus', _focusListener);
7584
domWindow.removeEventListener('blur', _blurListener);
76-
domWindow.removeEventListener('beforeunload', _beforeUnloadListener);
7785
domDocument.removeEventListener(
7886
'visibilitychange',
7987
_visibilityChangeListener,
8088
);
89+
for (final StreamSubscription<void> subscription in _subscriptions) {
90+
subscription.cancel();
91+
}
92+
_subscriptions.clear();
8193
}
8294

8395
late final DomEventListener _focusListener =
@@ -90,11 +102,6 @@ class _BrowserAppLifecycleState extends AppLifecycleState {
90102
onAppLifecycleStateChange(ui.AppLifecycleState.inactive);
91103
});
92104

93-
late final DomEventListener _beforeUnloadListener =
94-
createDomEventListener((DomEvent event) {
95-
onAppLifecycleStateChange(ui.AppLifecycleState.detached);
96-
});
97-
98105
late final DomEventListener _visibilityChangeListener =
99106
createDomEventListener((DomEvent event) {
100107
if (domDocument.visibilityState == 'visible') {
@@ -103,4 +110,12 @@ class _BrowserAppLifecycleState extends AppLifecycleState {
103110
onAppLifecycleStateChange(ui.AppLifecycleState.hidden);
104111
}
105112
});
113+
114+
void _onViewCountChanged(_) {
115+
if (_viewManager.views.isEmpty) {
116+
onAppLifecycleStateChange(ui.AppLifecycleState.detached);
117+
} else {
118+
onAppLifecycleStateChange(ui.AppLifecycleState.resumed);
119+
}
120+
}
106121
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
5+
import 'package:test/bootstrap/browser.dart';
6+
import 'package:test/test.dart';
7+
import 'package:ui/src/engine.dart';
8+
import 'package:ui/ui.dart' as ui;
9+
10+
void main() {
11+
internalBootstrapBrowserTest(() => testMain);
12+
}
13+
14+
void testMain() {
15+
group(AppLifecycleState, () {
16+
test('listens to changes in view manager', () {
17+
final FlutterViewManager viewManager = FlutterViewManager(EnginePlatformDispatcher.instance);
18+
final AppLifecycleState state = AppLifecycleState.create(viewManager);
19+
20+
ui.AppLifecycleState? currentState;
21+
void listener(ui.AppLifecycleState newState) {
22+
currentState = newState;
23+
}
24+
25+
state.addListener(listener);
26+
27+
final view1 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement());
28+
viewManager.registerView(view1);
29+
expect(currentState, ui.AppLifecycleState.resumed);
30+
currentState = null;
31+
32+
final view2 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement());
33+
viewManager.registerView(view2);
34+
// The listener should not be called again. The view manager is still not empty.
35+
expect(currentState, isNull);
36+
37+
viewManager.disposeAndUnregisterView(view1.viewId);
38+
// The listener should not be called again. The view manager is still not empty.
39+
expect(currentState, isNull);
40+
41+
viewManager.disposeAndUnregisterView(view2.viewId);
42+
expect(currentState, ui.AppLifecycleState.detached);
43+
currentState = null;
44+
45+
final view3 = EngineFlutterView(EnginePlatformDispatcher.instance, createDomHTMLDivElement());
46+
viewManager.registerView(view3);
47+
// The state should go back to `resumed` after a new view is registered.
48+
expect(currentState, ui.AppLifecycleState.resumed);
49+
50+
viewManager.dispose();
51+
state.removeListener(listener);
52+
});
53+
});
54+
}

0 commit comments

Comments
 (0)