Skip to content

Commit 61812ca

Browse files
Add platform check to FocusManager app lifecycle listener (#144718)
This PR implements a temporary fix for the mobile device keyboard bug reported in [this comment](flutter/flutter#142930 (comment)). CC @gspencergoog
1 parent 1ca8873 commit 61812ca

File tree

2 files changed

+51
-6
lines changed

2 files changed

+51
-6
lines changed

packages/flutter/lib/src/widgets/focus_manager.dart

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,8 +1677,16 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
16771677
if (kFlutterMemoryAllocationsEnabled) {
16781678
ChangeNotifier.maybeDispatchObjectCreation(this);
16791679
}
1680-
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
1681-
WidgetsBinding.instance.addObserver(_appLifecycleListener);
1680+
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
1681+
// It appears that some Android keyboard implementations can cause
1682+
// app lifecycle state changes: adding this listener would cause the
1683+
// text field to unfocus as the user is trying to type.
1684+
//
1685+
// Until this is resolved, we won't be adding the listener to Android apps.
1686+
// https://github.com/flutter/flutter/pull/142930#issuecomment-1981750069
1687+
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
1688+
WidgetsBinding.instance.addObserver(_appLifecycleListener!);
1689+
}
16821690
rootScope._manager = this;
16831691
}
16841692

@@ -1695,7 +1703,9 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
16951703

16961704
@override
16971705
void dispose() {
1698-
WidgetsBinding.instance.removeObserver(_appLifecycleListener);
1706+
if (_appLifecycleListener != null) {
1707+
WidgetsBinding.instance.removeObserver(_appLifecycleListener!);
1708+
}
16991709
_highlightManager.dispose();
17001710
rootScope.dispose();
17011711
super.dispose();
@@ -1856,7 +1866,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
18561866

18571867
// Allows FocusManager to respond to app lifecycle state changes,
18581868
// temporarily suspending the primaryFocus when the app is inactive.
1859-
late final _AppLifecycleListener _appLifecycleListener;
1869+
_AppLifecycleListener? _appLifecycleListener;
18601870

18611871
// Stores the node that was focused before the app lifecycle changed.
18621872
// Will be restored as the primary focus once app is resumed.

packages/flutter/test/widgets/focus_manager_test.dart

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,44 @@ void main() {
354354
logs.clear();
355355
}, variant: KeySimulatorTransitModeVariant.all());
356356

357+
testWidgets('FocusManager ignores app lifecycle changes on Android.', (WidgetTester tester) async {
358+
final bool shouldRespond = kIsWeb || defaultTargetPlatform != TargetPlatform.android;
359+
if (shouldRespond) {
360+
return;
361+
}
362+
363+
Future<void> setAppLifecycleState(AppLifecycleState state) async {
364+
final ByteData? message = const StringCodec().encodeMessage(state.toString());
365+
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
366+
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
367+
}
368+
369+
final BuildContext context = await setupWidget(tester);
370+
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
371+
addTearDown(scope.dispose);
372+
final FocusAttachment scopeAttachment = scope.attach(context);
373+
final FocusNode focusNode = FocusNode(debugLabel: 'Focus Node');
374+
addTearDown(focusNode.dispose);
375+
final FocusAttachment focusNodeAttachment = focusNode.attach(context);
376+
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
377+
focusNodeAttachment.reparent(parent: scope);
378+
focusNode.requestFocus();
379+
await tester.pump();
380+
expect(focusNode.hasPrimaryFocus, isTrue);
381+
382+
await setAppLifecycleState(AppLifecycleState.paused);
383+
expect(focusNode.hasPrimaryFocus, isTrue);
384+
385+
await setAppLifecycleState(AppLifecycleState.resumed);
386+
expect(focusNode.hasPrimaryFocus, isTrue);
387+
});
388+
357389
testWidgets('FocusManager responds to app lifecycle changes.', (WidgetTester tester) async {
390+
final bool shouldRespond = kIsWeb || defaultTargetPlatform != TargetPlatform.android;
391+
if (!shouldRespond) {
392+
return;
393+
}
394+
358395
Future<void> setAppLifecycleState(AppLifecycleState state) async {
359396
final ByteData? message = const StringCodec().encodeMessage(state.toString());
360397
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
@@ -402,8 +439,6 @@ void main() {
402439
expect(focusNode.hasPrimaryFocus, isTrue);
403440

404441
await setAppLifecycleState(AppLifecycleState.paused);
405-
expect(focusNode.hasPrimaryFocus, isFalse);
406-
407442
focusNodeAttachment.detach();
408443
expect(focusNode.hasPrimaryFocus, isFalse);
409444

0 commit comments

Comments
 (0)