I have been struggling with debugging why nearly all of my TextField
s and TextFormField
s are breaking in my Flutter app. By breaking, I mean:
I would assume that the latter of these issues is a consequence of some error which can be resolved by fixing the former. One of the situations where this error arises is in a widget I made called VerifyAlert
, which displays a TextField
and Ok/Cancel action buttons in an AlertDialog
. Here is the implementation:
class VerifyAlert extends StatefulWidget {
const VerifyAlert({
super.key,
this.message = 'Verify',
this.confirmMessage = 'Ok',
this.cancelMessage = 'Cancel',
required this.onVerification,
required this.onFailedVerification,
required this.verifier,
});
final String message; // body text
final String confirmMessage; // confirm button text
final String cancelMessage; // cancel button text
final AsyncCallback onVerification;
final AsyncCallback onFailedVerification;
final Function(String) verifier;
@override
State<VerifyAlert> createState() => _VerifyAlertState();
}
class _VerifyAlertState extends State<VerifyAlert> {
final _controller = TextEditingController();
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(
widget.message,
textAlign: TextAlign.center,
style: GoogleFonts.raleway(
fontWeight: FontWeight.w400,
fontSize: 24.0,
),
),
//* Auth Password
content: TextFormField(
controller: _controller,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
hintText: 'Empty',
),
autocorrect: false,
autofocus: false,
),
actions: [
TextButton(
child: Text(
widget.cancelMessage,
style: const TextStyle(
fontSize: 17.5,
color: ui.LightScheme.blue,
),
),
onPressed: () {
widget.onFailedVerification();
},
),
TextButton(
onPressed: () async {
if (widget.verifier(_controller.text)) {
if (context.mounted) {
widget.onVerification();
}
} else {
if (context.mounted) {
widget.onFailedVerification();
}
}
},
child: Text(
widget.confirmMessage,
style: const TextStyle(
fontSize: 17.5,
color: ui.LightScheme.blue,
),
),
),
],
);
}
}
I have another method for showing this alert:
Future<void> showVerifyAlert({
required BuildContext context,
String message = '',
String confirmMessage = 'Ok',
String cancelMessage = 'Cancel',
required AsyncCallback onVerification,
required AsyncCallback onFailedVerification,
required Function(String) verifier,
}) async {
return await showDialog(
context: context,
builder: (context) {
return VerifyAlert(
message: message,
confirmMessage: confirmMessage,
cancelMessage: cancelMessage,
onVerification: onVerification,
onFailedVerification: onFailedVerification,
verifier: verifier,
);
},
barrierDismissible: true,
);
}
I have verified before that this scheme of constructing custom alerts works, so the issue is definitely with the way my TextFormField
. Also, this is not the only instance where a TextField
or TextFormField
breaks in my application; nearly all non-trivial implementations of Text(Form)Field suffer from the same issue.
My environment is an M1 Macbook Pro, iOS Simulator running iOS 16.4 emulating an iPad (10th Generation), and Flutter 3.14.0-0.1.pre with Dart SDK version: 3.2.0-42.1.beta.
I tried to re-implement my Text(Form)Fields without a TextEditingController
, but the problem still remained. In the context of the example given previously, I rewrote the TextFormField
to have no controller
and, instead, to have an onChanged
method which set the state of a string which stored the current value in the input, which I could then use in the verifier
method.
I have also tried to use ValueKey
s in my fields, but this has also failed.
Previously, I had a very similar problem with one of my TextFormField
s, and all I did was move to the Flutter beta channel, install an XCode update, and restart my computer, and without changing my code at all, the TextFormField now worked. This was very strange, but I ignored it for the time being. Now, this problem has permeated nearly all of my Text(Form)Fields, and so, I am unable to ascertain whether or not this is a problem with my code or a problem with my environment.
Having spent nearly a week on this issue, I hope someone in this community can help. Thanks.
Update 1
I built another application with this VerifyAlert
widget, and it worked as expected, so I am inclined to believe this is an issue with the way Flutter is either focusing or building my Text(Form)Fields. This same issue is prevalent in both iOS and Android emulators of all types, and on my physical device.
The only major difference between my actual application and the application I built to test VerifyAlert
is that in the former, I am showing my alert after two layers of pushed contexts (i.e., I've called Navigator.push(context, MaterialPageRoute(...))
twice before I show my alert). I am inclined to believe that it is some behavior confused from these contexts that is causing the bug I am experiencing.
As a suggestion recommended, I've attached the code which triggers this VerifyAlert
:
class RecordsView extends ConsumerStatefulWidget {
const RecordsView({
super.key,
this.record,
required this.accessType,
});
final Map<String, dynamic>? record;
final RecordAccess accessType;
@override
ConsumerState<ConsumerStatefulWidget> createState() => _RecordsViewState();
}
class _RecordsViewState extends ConsumerState<RecordsView> {
//* Method for initializing record and returning a Future<bool> of whether or not it was successful
Future<bool> initializeRecord(
SelectedForm selectedForm,
User user,
) async {
try {
if (widget.accessType == RecordAccess.create) {
Map<String, dynamic> formData =
await network.NetworkUtility.fetchFormDetails(
accessToken: user.accessToken,
formId: selectedForm.id.toString(),
);
Map<String, dynamic> pagesData =
await network.NetworkUtility.fetchFormPageDetails(
formId: selectedForm.id.toString(),
accessToken: user.accessToken,
);
if (Record.isCreateValid(formData: formData, pagesData: pagesData)) {
ref.read(recordProvider.notifier).clear();
ref.read(recordProvider.notifier).create(
formData: formData,
pagesData: pagesData,
accessType: widget.accessType,
);
return true;
} else {
return false;
}
} else {
if (widget.record == null) {
return false;
} else {
int formId = selectedForm.id;
if (selectedForm.id == -1) {
try {
formId = widget.record!['form'] as int;
} catch (e) {
return false;
}
}
Map<String, dynamic> formData =
await network.NetworkUtility.fetchFormDetails(
accessToken: user.accessToken,
formId: formId.toString(),
);
Map<String, dynamic> pagesData =
await network.NetworkUtility.fetchFormPageDetails(
formId: formId.toString(),
accessToken: user.accessToken,
);
if (Record.isLoadValid(
formData: formData,
pagesData: pagesData,
recordData: widget.record!,
)) {
ref.read(recordProvider.notifier).clear();
ref.read(recordProvider.notifier).load(
formData: formData,
pagesData: pagesData,
recordData: widget.record!,
accessType: widget.accessType,
);
if (ref
.read(recordProvider)
.auth['password']
.toString()
.isNotEmpty) {
bool verified = false;
if (context.mounted) {
await ui.showVerifyAlert(
context: GlobalKeys.alpha.currentContext!,
message: 'Enter Password',
onVerification: () {
verified = true;
},
onFailedVerification: () {},
verifier: (value) {
return value ==
ref.read(recordProvider).auth['password'].toString();
},
);
}
return verified;
} else {
return true;
}
} else {
return false;
}
}
}
} catch (e) {
return false;
}
}
Widget closableContent(
BuildContext context,
Widget centerWidget,
) {
return Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
InkWell(
onTap: () {
Navigator.of(context).pop(false);
},
child: Icon(Icons.close, size: ui.Text.scale(context, 36)),
),
],
),
Expanded(
child: Center(
child: centerWidget,
),
),
],
);
}
@override
Widget build(BuildContext context) {
final selectedForm = ref.watch(selectedFormProvider);
final user = ref.watch(userProvider);
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
resizeToAvoidBottomInset:
false, // ensures that the keyboard does not push UI elements up
backgroundColor: ui.LightScheme.gray,
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(ui.Text.scale(context, 21, 3)),
//* All Content is in SafeArea and Padded
child: FutureBuilder(
future: initializeRecord(selectedForm, user),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data!) {
return const RecordVisualizer();
} else {
return closableContent(
context,
const Text('Authentication Failed. Please try again.'),
);
}
} else if (snapshot.hasError) {
return closableContent(
context,
const Text('Could not load record. Please try again.'),
);
} else {
return closableContent(
context,
const ui.FourDotsLoadingIndicator(),
);
}
},
),
),
),
),
);
}
}
This widget is pushed on top of another widget, which itself is also pushed onto the first widget in the widget tree. Perhaps this is the reason behind my bug, but I am still unable to determine how to proceed from here. This issue is likely not originating from anything in Riverpod, as VerifyAlert
breaks even when there is nothing related to Riverpod embedded in its logic. I would appreciate any additional comments and guidance, and thank you once again.
I was able to figure out why this problem was happening, and I think posting an answer would be instructive for anyone running into similar issues in the future, especially since there is no clear error Flutter provides for such behavior and it is not something easily identifiable by just observing one's code.
There were two primary issues in my code:
Media Queries: I was using custom media queries to size parts of the UI. For some reason, whenever a TextField is active in particularly complex UI arrangements (which may just happen accidentally), the media query for resizing your UI is called over and over again, causing repeated builds and make your app work entirely unpredictably. The best way to diagnose if this is your issue is to first check how many times your build()
function is executing (add a print('built')
inside it, for example), and if it is building excessively, replace your media queries with the Sizer package.
Riverpod & TextField Focus Returning: A problem was happening in another part of my code involving TextFields where a Riverpod state change would cause the TextField's focus to return to the previous TextField upon which the user was focused. The only way I was able to resolve this issue was by adding a FocusNode
to each TextField widget and by wrapping the TextField in a GestureDetector which had the following function inside it:
onTap: () {
FocusManager.instance.primaryFocus?.unfocus();
FocusScope.of(context).requestFocus(_focus);
}
Here, _focus
refers to a private FocusNode
attribute. What this does is to force the current focus to be cleared and assigns the focus back to your TextField
, which should behave just like a TextField
normally would; but, for some reason, it doesn't and instead fixes this issue with Riverpod
and TextFields
interacting in an undesirable manner.