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

[web] Implement AppLifecycleState.detached as documented #53506

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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((_) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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') {
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
}