Skip to content

Commit 7e71fa7

Browse files
committed
fix(authenticator): defer controller->state updates to post-frame and stabilize sync
Buffer controller changes and apply them in a post-frame callback to avoid calling setState during build. Normalize trailing whitespace when comparing controller and state to reduce unnecessary writes. Ensure didUpdateWidget sync runs after build and clear pending state on dispose. Add widget tests covering typing, special keys, rapid updates, and controller interactions.
1 parent 0db4572 commit 7e71fa7

File tree

3 files changed

+306
-21
lines changed

3 files changed

+306
-21
lines changed

packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_text_field.dart

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import 'package:amplify_authenticator/src/widgets/form.dart';
55
import 'package:amplify_authenticator/src/widgets/form_field.dart';
66
import 'package:flutter/material.dart';
7+
import 'package:flutter/scheduler.dart';
78
import 'package:flutter/services.dart';
89

910
mixin AuthenticatorTextField<
@@ -14,6 +15,8 @@ mixin AuthenticatorTextField<
1415
TextEditingController? _effectiveController;
1516
bool _isApplyingControllerText = false;
1617
String? _lastSyncedControllerValue;
18+
String? _pendingControllerText;
19+
bool _controllerUpdateScheduled = false;
1720

1821
@protected
1922
TextEditingController? get textController => null;
@@ -26,6 +29,7 @@ mixin AuthenticatorTextField<
2629
_effectiveController?.removeListener(_handleControllerChanged);
2730
_effectiveController = controller;
2831
_lastSyncedControllerValue = null;
32+
_pendingControllerText = null;
2933
if (_effectiveController != null) {
3034
_effectiveController!.addListener(_handleControllerChanged);
3135
}
@@ -37,11 +41,27 @@ mixin AuthenticatorTextField<
3741
return;
3842
}
3943
final text = controller.text;
40-
if (text == _lastSyncedControllerValue) {
44+
if (text == _lastSyncedControllerValue && _pendingControllerText == null) {
4145
return;
4246
}
43-
_lastSyncedControllerValue = text;
44-
onChanged(text);
47+
_pendingControllerText = text;
48+
if (_controllerUpdateScheduled) {
49+
return;
50+
}
51+
_controllerUpdateScheduled = true;
52+
SchedulerBinding.instance.addPostFrameCallback((_) {
53+
_controllerUpdateScheduled = false;
54+
final pendingText = _pendingControllerText;
55+
_pendingControllerText = null;
56+
if (!mounted || pendingText == null) {
57+
return;
58+
}
59+
if (pendingText == _lastSyncedControllerValue) {
60+
return;
61+
}
62+
_lastSyncedControllerValue = pendingText;
63+
onChanged(pendingText);
64+
});
4565
}
4666

4767
void _syncControllerText({bool force = false}) {
@@ -50,8 +70,16 @@ mixin AuthenticatorTextField<
5070
return;
5171
}
5272
final target = initialValue ?? '';
53-
if (!force && controller.text == target) {
54-
_lastSyncedControllerValue = controller.text;
73+
final controllerText = controller.text;
74+
if (!force && controllerText == target) {
75+
_lastSyncedControllerValue = controllerText;
76+
return;
77+
}
78+
79+
final normalizedController = controllerText.trimRight();
80+
final normalizedTarget = target.trimRight();
81+
if (normalizedController == normalizedTarget) {
82+
_lastSyncedControllerValue = controllerText;
5583
return;
5684
}
5785
_isApplyingControllerText = true;
@@ -61,6 +89,7 @@ mixin AuthenticatorTextField<
6189
composing: TextRange.empty,
6290
);
6391
_lastSyncedControllerValue = target;
92+
_pendingControllerText = null;
6493
_isApplyingControllerText = false;
6594
}
6695

@@ -93,13 +122,19 @@ mixin AuthenticatorTextField<
93122
void didUpdateWidget(covariant T oldWidget) {
94123
super.didUpdateWidget(oldWidget);
95124
_maybeUpdateEffectiveController();
96-
_syncControllerText(force: true);
125+
WidgetsBinding.instance.addPostFrameCallback((_) {
126+
if (mounted) {
127+
_syncControllerText(force: true);
128+
}
129+
});
97130
}
98131

99132
@override
100133
void dispose() {
101134
_effectiveController?.removeListener(_handleControllerChanged);
102135
_effectiveController = null;
136+
_pendingControllerText = null;
137+
_controllerUpdateScheduled = false;
103138
super.dispose();
104139
}
105140

@@ -117,6 +152,7 @@ mixin AuthenticatorTextField<
117152
builder: (BuildContext context, bool toggleObscureText, Widget? _) {
118153
final obscureText = this.obscureText && toggleObscureText;
119154
// Don't sync during build
155+
final shouldHandleChangeImmediately = _effectiveController == null;
120156
return TextFormField(
121157
style: enabled
122158
? null
@@ -125,7 +161,7 @@ mixin AuthenticatorTextField<
125161
initialValue: _effectiveController == null ? initialValue : null,
126162
enabled: enabled,
127163
validator: widget.validatorOverride ?? validator,
128-
onChanged: onChanged,
164+
onChanged: shouldHandleChangeImmediately ? onChanged : null,
129165
autocorrect: false,
130166
decoration: InputDecoration(
131167
labelText: labelText,

packages/authenticator/amplify_authenticator/lib/src/mixins/authenticator_username_field.dart

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:amplify_authenticator/src/utils/validators.dart';
99
import 'package:amplify_authenticator/src/widgets/component.dart';
1010
import 'package:amplify_authenticator/src/widgets/form_field.dart';
1111
import 'package:flutter/material.dart';
12+
import 'package:flutter/scheduler.dart';
1213

1314
mixin AuthenticatorUsernameField<
1415
FieldType extends Enum,
@@ -19,6 +20,8 @@ mixin AuthenticatorUsernameField<
1920
UsernameType? _controllerUsernameType;
2021
bool _applyingControllerText = false;
2122
String? _lastSyncedText;
23+
String? _pendingControllerText;
24+
bool _controllerUpdateScheduled = false;
2225

2326
@protected
2427
TextEditingController? get textController => null;
@@ -42,6 +45,7 @@ mixin AuthenticatorUsernameField<
4245
_controller = controller;
4346
_controllerUsernameType = type;
4447
_lastSyncedText = null;
48+
_pendingControllerText = null;
4549

4650
if (_controller != null && shouldListen) {
4751
_controller!.addListener(_handleControllerChanged);
@@ -55,17 +59,35 @@ mixin AuthenticatorUsernameField<
5559
}
5660

5761
final text = controller.text;
58-
if (text == _lastSyncedText) {
62+
if (text == _lastSyncedText && _pendingControllerText == null) {
5963
return;
6064
}
6165

62-
_lastSyncedText = text;
63-
_applyingControllerText = true;
64-
try {
65-
onChanged(UsernameInput(type: selectedUsernameType, username: text));
66-
} finally {
67-
_applyingControllerText = false;
66+
_pendingControllerText = text;
67+
if (_controllerUpdateScheduled) {
68+
return;
6869
}
70+
_controllerUpdateScheduled = true;
71+
SchedulerBinding.instance.addPostFrameCallback((_) {
72+
_controllerUpdateScheduled = false;
73+
final pendingText = _pendingControllerText;
74+
_pendingControllerText = null;
75+
if (!mounted || pendingText == null) {
76+
return;
77+
}
78+
if (pendingText == _lastSyncedText) {
79+
return;
80+
}
81+
_applyingControllerText = true;
82+
try {
83+
onChanged(
84+
UsernameInput(type: selectedUsernameType, username: pendingText),
85+
);
86+
_lastSyncedText = pendingText;
87+
} finally {
88+
_applyingControllerText = false;
89+
}
90+
});
6991
}
7092

7193
void _syncControllerText({bool force = false}) {
@@ -75,8 +97,16 @@ mixin AuthenticatorUsernameField<
7597
}
7698

7799
final target = initialValue?.username ?? '';
78-
if (!force && _controller!.text == target) {
79-
_lastSyncedText = _controller!.text;
100+
final controllerText = _controller!.text;
101+
if (!force && controllerText == target) {
102+
_lastSyncedText = controllerText;
103+
return;
104+
}
105+
106+
final normalizedController = controllerText.trimRight();
107+
final normalizedTarget = target.trimRight();
108+
if (normalizedController == normalizedTarget) {
109+
_lastSyncedText = controllerText;
80110
return;
81111
}
82112

@@ -87,6 +117,7 @@ mixin AuthenticatorUsernameField<
87117
composing: TextRange.empty,
88118
);
89119
_lastSyncedText = target;
120+
_pendingControllerText = null;
90121
_applyingControllerText = false;
91122
}
92123

@@ -118,13 +149,19 @@ mixin AuthenticatorUsernameField<
118149
void didUpdateWidget(covariant T oldWidget) {
119150
super.didUpdateWidget(oldWidget);
120151
_updateController();
121-
_syncControllerText(force: true);
152+
WidgetsBinding.instance.addPostFrameCallback((_) {
153+
if (mounted) {
154+
_syncControllerText(force: true);
155+
}
156+
});
122157
}
123158

124159
@override
125160
void dispose() {
126161
_controller?.removeListener(_handleControllerChanged);
127162
_controller = null;
163+
_pendingControllerText = null;
164+
_controllerUpdateScheduled = false;
128165
super.dispose();
129166
}
130167

@@ -310,8 +347,8 @@ mixin AuthenticatorUsernameField<
310347
final inputResolver = stringResolver.inputs;
311348
final hintText = inputResolver.resolve(context, hintKey);
312349

313-
void onChanged(String username) {
314-
return this.onChanged(
350+
void handleChanged(String username) {
351+
return onChanged(
315352
UsernameInput(type: selectedUsernameType, username: username),
316353
);
317354
}
@@ -327,7 +364,7 @@ mixin AuthenticatorUsernameField<
327364
return AuthenticatorPhoneField<FieldType>(
328365
field: widget.field,
329366
requiredOverride: true,
330-
onChanged: onChanged,
367+
onChanged: handleChanged,
331368
validator: validator,
332369
enabled: enabled,
333370
errorMaxLines: errorMaxLines,
@@ -340,13 +377,19 @@ mixin AuthenticatorUsernameField<
340377
_updateController();
341378
// Don't sync during build
342379

380+
final controllerInUse = _controller != null;
381+
343382
return TextFormField(
344383
style: enabled ? null : TextStyle(color: Theme.of(context).disabledColor),
345384
controller: _controller,
346385
initialValue: _controller == null ? initialValue?.username : null,
347386
enabled: enabled,
348387
validator: validator,
349-
onChanged: onChanged,
388+
onChanged: (username) {
389+
if (!controllerInUse) {
390+
handleChanged(username);
391+
}
392+
},
350393
autocorrect: false,
351394
decoration: InputDecoration(
352395
prefixIcon: prefix,

0 commit comments

Comments
 (0)