Skip to content

Commit

Permalink
Add platform check to FocusManager app lifecycle listener (#144718)
Browse files Browse the repository at this point in the history
This PR implements a temporary fix for the mobile device keyboard bug reported in [this comment](flutter/flutter#142930 (comment)).

CC @gspencergoog
  • Loading branch information
nate-thegrate authored Mar 12, 2024
1 parent 1ca8873 commit 61812ca
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 6 deletions.
18 changes: 14 additions & 4 deletions packages/flutter/lib/src/widgets/focus_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1677,8 +1677,16 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
}
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
WidgetsBinding.instance.addObserver(_appLifecycleListener);
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
_appLifecycleListener = _AppLifecycleListener(_appLifecycleChange);
WidgetsBinding.instance.addObserver(_appLifecycleListener!);
}
rootScope._manager = this;
}

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

@override
void dispose() {
WidgetsBinding.instance.removeObserver(_appLifecycleListener);
if (_appLifecycleListener != null) {
WidgetsBinding.instance.removeObserver(_appLifecycleListener!);
}
_highlightManager.dispose();
rootScope.dispose();
super.dispose();
Expand Down Expand Up @@ -1856,7 +1866,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {

// Allows FocusManager to respond to app lifecycle state changes,
// temporarily suspending the primaryFocus when the app is inactive.
late final _AppLifecycleListener _appLifecycleListener;
_AppLifecycleListener? _appLifecycleListener;

// Stores the node that was focused before the app lifecycle changed.
// Will be restored as the primary focus once app is resumed.
Expand Down
39 changes: 37 additions & 2 deletions packages/flutter/test/widgets/focus_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,44 @@ 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;
}

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);

await setAppLifecycleState(AppLifecycleState.paused);
expect(focusNode.hasPrimaryFocus, isTrue);

await setAppLifecycleState(AppLifecycleState.resumed);
expect(focusNode.hasPrimaryFocus, isTrue);
});

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
Expand Down Expand Up @@ -402,8 +439,6 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue);

await setAppLifecycleState(AppLifecycleState.paused);
expect(focusNode.hasPrimaryFocus, isFalse);

focusNodeAttachment.detach();
expect(focusNode.hasPrimaryFocus, isFalse);

Expand Down

0 comments on commit 61812ca

Please sign in to comment.