Reland "Update FocusManager platform check to include iOS" (#148984)

It looks like removing `kIsWeb` from the `FocusManager._appLifecycleListener` platform check is causing [memory leaks](https://github.com/flutter/flutter/issues/148985) and test failures.

This pull request fixes #148475 and prevents the test failures shown in #148978.
This commit is contained in:
Nate 2024-05-23 17:13:18 -06:00 committed by GitHub
parent 5beac73cfa
commit f419177057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 48 deletions

View file

@ -1532,19 +1532,29 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
}
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
// It appears that some Android keyboard implementations can cause
// app lifecycle state changes: adding this listener would cause the
// text field to unfocus as the user is trying to type.
//
// Until this is resolved, we won't be adding the listener to Android apps.
// https://github.com/flutter/flutter/pull/142930#issuecomment-1981750069
if (_respondToWindowFocus) {
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
WidgetsBinding.instance.addObserver(_appLifecycleListener!);
}
rootScope._manager = this;
}
/// It appears that some Android keyboard implementations can cause
/// app lifecycle state changes: adding the app lifecycle listener would
/// cause the text field to unfocus as the user is trying to type.
///
/// Additionally, on iOS, input fields aren't automatically populated
/// with relevant data when using autofill.
///
/// Until these are resolved, we won't be adding the listener to mobile platforms.
/// https://github.com/flutter/flutter/issues/148475#issuecomment-2118407411
/// https://github.com/flutter/flutter/pull/142930#issuecomment-1981750069
bool get _respondToWindowFocus => kIsWeb || switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS => false,
TargetPlatform.fuchsia || TargetPlatform.linux => true,
TargetPlatform.windows || TargetPlatform.macOS => true,
};
/// Registers global input event handlers that are needed to manage focus.
///
/// This calls the [HardwareKeyboard.addHandler] on the shared instance of
@ -1875,7 +1885,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// Enables this [FocusManager] to listen to changes of the application
/// lifecycle if it does not already have an application lifecycle listener
/// active, and the current platform is detected as [kIsWeb] or non-Android.
/// active, and the app isn't running on a native mobile platform.
///
/// Typically, the application lifecycle listener for this [FocusManager] is
/// setup at construction, but sometimes it is necessary to manually initialize
@ -1889,13 +1899,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// supported.
@visibleForTesting
void listenToApplicationLifecycleChangesIfSupported() {
if (_appLifecycleListener == null && (kIsWeb || defaultTargetPlatform != TargetPlatform.android)) {
// It appears that some Android keyboard implementations can cause
// app lifecycle state changes: adding this listener would cause the
// text field to unfocus as the user is trying to type.
//
// Until this is resolved, we won't be adding the listener to Android apps.
// https://github.com/flutter/flutter/pull/142930#issuecomment-1981750069
if (_appLifecycleListener == null && _respondToWindowFocus) {
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
WidgetsBinding.instance.addObserver(_appLifecycleListener!);
}

View file

@ -354,44 +354,39 @@ void main() {
logs.clear();
}, variant: KeySimulatorTransitModeVariant.all());
testWidgets('FocusManager ignores app lifecycle changes on Android.', (WidgetTester tester) async {
final bool shouldRespond = kIsWeb || defaultTargetPlatform != TargetPlatform.android;
if (shouldRespond) {
return;
}
testWidgets(
'FocusManager ignores app lifecycle changes on Android and iOS.',
(WidgetTester tester) async {
Future<void> setAppLifecycleState(AppLifecycleState state) async {
final ByteData? message = const StringCodec().encodeMessage(state.toString());
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
}
Future<void> setAppLifecycleState(AppLifecycleState state) async {
final ByteData? message = const StringCodec().encodeMessage(state.toString());
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
}
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode focusNode = FocusNode(debugLabel: 'Focus Node');
addTearDown(focusNode.dispose);
final FocusAttachment focusNodeAttachment = focusNode.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
focusNodeAttachment.reparent(parent: scope);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
addTearDown(scope.dispose);
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode focusNode = FocusNode(debugLabel: 'Focus Node');
addTearDown(focusNode.dispose);
final FocusAttachment focusNodeAttachment = focusNode.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
focusNodeAttachment.reparent(parent: scope);
focusNode.requestFocus();
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await setAppLifecycleState(AppLifecycleState.paused);
expect(focusNode.hasPrimaryFocus, isTrue);
await setAppLifecycleState(AppLifecycleState.paused);
expect(focusNode.hasPrimaryFocus, isTrue);
await setAppLifecycleState(AppLifecycleState.resumed);
expect(focusNode.hasPrimaryFocus, isTrue);
});
await setAppLifecycleState(AppLifecycleState.resumed);
expect(focusNode.hasPrimaryFocus, isTrue);
},
skip: kIsWeb, // [intended]
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android, TargetPlatform.iOS}),
);
testWidgets('FocusManager responds to app lifecycle changes.', (WidgetTester tester) async {
final bool shouldRespond = kIsWeb || defaultTargetPlatform != TargetPlatform.android;
if (!shouldRespond) {
return;
}
Future<void> setAppLifecycleState(AppLifecycleState state) async {
final ByteData? message = const StringCodec().encodeMessage(state.toString());
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger