Skip to content

Commit c9d70b6

Browse files
xu-baolinchristopherfujino
authored andcommitted
Fix the inconsistency between the local state of the input and the engine state (#65754)
1 parent 7e354ec commit c9d70b6

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,11 +1620,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16201620
@override
16211621
TextEditingValue get currentTextEditingValue => _value;
16221622

1623+
bool _updateEditingValueInProgress = false;
1624+
16231625
@override
16241626
void updateEditingValue(TextEditingValue value) {
1627+
_updateEditingValueInProgress = true;
16251628
// Since we still have to support keyboard select, this is the best place
16261629
// to disable text updating.
16271630
if (!_shouldCreateInputConnection) {
1631+
_updateEditingValueInProgress = false;
16281632
return;
16291633
}
16301634
if (widget.readOnly) {
@@ -1643,7 +1647,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16431647
}
16441648
}
16451649

1646-
if (_isSelectionOnlyChange(value)) {
1650+
if (value == _value) {
1651+
// This is possible, for example, when the numeric keyboard is input,
1652+
// the engine will notify twice for the same value.
1653+
// Track at https://github.com/flutter/flutter/issues/65811
1654+
_updateEditingValueInProgress = false;
1655+
return;
1656+
} else if (value.text == _value.text && value.composing == _value.composing && value.selection != _value.selection) {
1657+
// `selection` is the only change.
16471658
_handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard);
16481659
} else {
16491660
_formatAndSetValue(value);
@@ -1655,10 +1666,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16551666
_stopCursorTimer(resetCharTicks: false);
16561667
_startCursorTimer();
16571668
}
1658-
}
1659-
1660-
bool _isSelectionOnlyChange(TextEditingValue value) {
1661-
return value.text == _value.text && value.composing == _value.composing;
1669+
_updateEditingValueInProgress = false;
16621670
}
16631671

16641672
@override
@@ -1815,8 +1823,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18151823
if (!_hasInputConnection)
18161824
return;
18171825
final TextEditingValue localValue = _value;
1818-
if (localValue == _receivedRemoteTextEditingValue)
1826+
// We should not update back the value notified by the remote(engine) in reverse, this is redundant.
1827+
// Unless we modify this value for some reason during processing, such as `TextInputFormatter`.
1828+
if (_updateEditingValueInProgress && localValue == _receivedRemoteTextEditingValue)
18191829
return;
1830+
// In other cases, as long as the value of the [widget.controller.value] is modified,
1831+
// `setEditingState` should be called as we do not want to skip sending real changes
1832+
// to the engine.
1833+
// Also see https://github.com/flutter/flutter/issues/65059#issuecomment-690254379
18201834
_textInputConnection.setEditingState(localValue);
18211835
}
18221836

@@ -2140,10 +2154,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
21402154
_value = _lastFormattedValue;
21412155
}
21422156

2143-
// Always attempt to send the value. If the value has changed, then it will send,
2144-
// otherwise, it will short-circuit.
2145-
_updateRemoteEditingValueIfNeeded();
2146-
21472157
if (textChanged && widget.onChanged != null)
21482158
widget.onChanged(value.text);
21492159
_lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;

packages/flutter/test/widgets/editable_text_test.dart

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4715,6 +4715,132 @@ void main() {
47154715
expect(tester.testTextInput.editingState['text'], 'flutter is the best!...');
47164716
});
47174717

4718+
testWidgets('Synchronous test of local and remote editing values', (WidgetTester tester) async {
4719+
// Regression test for https://github.com/flutter/flutter/issues/65059
4720+
final List<MethodCall> log = <MethodCall>[];
4721+
SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async {
4722+
log.add(methodCall);
4723+
});
4724+
final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {
4725+
if (newValue.text == 'I will be modified by the formatter.') {
4726+
newValue = const TextEditingValue(text: 'Flutter is the best!');
4727+
}
4728+
return newValue;
4729+
});
4730+
final TextEditingController controller = TextEditingController();
4731+
StateSetter setState;
4732+
4733+
final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Focus Node');
4734+
Widget builder() {
4735+
return StatefulBuilder(
4736+
builder: (BuildContext context, StateSetter setter) {
4737+
setState = setter;
4738+
return MaterialApp(
4739+
home: MediaQuery(
4740+
data: const MediaQueryData(devicePixelRatio: 1.0),
4741+
child: Directionality(
4742+
textDirection: TextDirection.ltr,
4743+
child: Center(
4744+
child: Material(
4745+
child: EditableText(
4746+
controller: controller,
4747+
focusNode: focusNode,
4748+
style: textStyle,
4749+
cursorColor: Colors.red,
4750+
backgroundCursorColor: Colors.red,
4751+
keyboardType: TextInputType.multiline,
4752+
inputFormatters: <TextInputFormatter>[
4753+
formatter,
4754+
],
4755+
onChanged: (String value) { },
4756+
),
4757+
),
4758+
),
4759+
),
4760+
),
4761+
);
4762+
},
4763+
);
4764+
}
4765+
4766+
await tester.pumpWidget(builder());
4767+
await tester.tap(find.byType(EditableText));
4768+
await tester.showKeyboard(find.byType(EditableText));
4769+
await tester.pump();
4770+
4771+
log.clear();
4772+
4773+
final EditableTextState state = tester.firstState(find.byType(EditableText));
4774+
4775+
// setEditingState is not called when only the remote changes
4776+
state.updateEditingValue(const TextEditingValue(
4777+
text: 'a',
4778+
));
4779+
expect(log.length, 0);
4780+
4781+
// setEditingState is called when remote value modified by the formatter.
4782+
state.updateEditingValue(const TextEditingValue(
4783+
text: 'I will be modified by the formatter.',
4784+
));
4785+
expect(log.length, 1);
4786+
MethodCall methodCall = log[0];
4787+
expect(
4788+
methodCall,
4789+
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
4790+
'text': 'Flutter is the best!',
4791+
'selectionBase': -1,
4792+
'selectionExtent': -1,
4793+
'selectionAffinity': 'TextAffinity.downstream',
4794+
'selectionIsDirectional': false,
4795+
'composingBase': -1,
4796+
'composingExtent': -1,
4797+
}),
4798+
);
4799+
4800+
log.clear();
4801+
4802+
// setEditingState is called when the [controller.value] is modified by local.
4803+
setState(() {
4804+
controller.text = 'I love flutter!';
4805+
});
4806+
expect(log.length, 1);
4807+
methodCall = log[0];
4808+
expect(
4809+
methodCall,
4810+
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
4811+
'text': 'I love flutter!',
4812+
'selectionBase': -1,
4813+
'selectionExtent': -1,
4814+
'selectionAffinity': 'TextAffinity.downstream',
4815+
'selectionIsDirectional': false,
4816+
'composingBase': -1,
4817+
'composingExtent': -1,
4818+
}),
4819+
);
4820+
4821+
log.clear();
4822+
4823+
// Currently `_receivedRemoteTextEditingValue` equals 'I will be modified by the formatter.',
4824+
// setEditingState will be called when set the [controller.value] to `_receivedRemoteTextEditingValue` by local.
4825+
setState(() {
4826+
controller.text = 'I will be modified by the formatter.';
4827+
});
4828+
expect(log.length, 1);
4829+
methodCall = log[0];
4830+
expect(
4831+
methodCall,
4832+
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
4833+
'text': 'I will be modified by the formatter.',
4834+
'selectionBase': -1,
4835+
'selectionExtent': -1,
4836+
'selectionAffinity': 'TextAffinity.downstream',
4837+
'selectionIsDirectional': false,
4838+
'composingBase': -1,
4839+
'composingExtent': -1,
4840+
}),
4841+
);
4842+
});
4843+
47184844
testWidgets('autofocus:true on first frame does not throw', (WidgetTester tester) async {
47194845
final TextEditingController controller = TextEditingController(text: testText);
47204846
controller.selection = const TextSelection(

0 commit comments

Comments
 (0)