mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Implement AppLifecycleState.detached as documented (flutter/engine#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 https://github.com/flutter/flutter/issues/150636 Fixes https://github.com/flutter/flutter/issues/149417
This commit is contained in:
parent
833ddf5993
commit
2d762001d3
@ -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<EngineFlutterView> get views => viewManager.views;
|
||||
|
||||
@ -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<StreamSubscription<void>> _subscriptions = <StreamSubscription<void>>[];
|
||||
|
||||
@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<void> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user