When building a custom form field by extending FormField (with a TextFormField centered within two IconButtons), I encounter unexpected issues with the keyboard.
After a state change due to a button press, the TextFormField is only updated when it has been provided with a key during construction (e.g. UniqueKey()). However, when using keys in this way, the keyboard directly closes after opening. The keyboard only works properly when no key is provided to TextFormField. When the key from the FormField baseclass constructor is provided, the keyboard also doesn't work and TextFormField is not updated when an IconButton is pressed.
When using the iOS simulator, no error is received when the keyboard closes directly after opening. When using a physical device, I get the following error:
Successfully load keyboard extensions
[lifecycle] [u 4EF1D37A-3BC9-4488-BC5A-A32FFFD4094F:m (null)] [com.google.keyboard.KeyboardExtension(2.3.19)] RB query for the extension process state failed with error: Error Domain=RBSServiceErrorDomain Code=1 "Client not entitled" UserInfo={RBSEntitlement=com.apple.runningboard.process-state, NSLocalizedFailureReason=Client not entitled, RBSPermanent=false}
The problem occurs at various versions of flutter, among which is version 3.7.0-1.3.pre.
Is this a bug or should the keys be used in another way?
Code that illustrates the problem
class CustomNumberFormField extends FormField<double> {
final int scale;
final TextStyle? textStyle;
CustomNumberFormField({
required FormFieldSetter<double> onSaved,
this.scale = 0,
this.textStyle,
super.validator,
super.key,
super.initialValue,
}) : super(
onSaved: onSaved,
builder: (FormFieldState<double> state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
state.didChange((state.value != null)
? state.value! - 1
: initialValue);
},
),
SizedBox(
width: 100,
child: TextFormField(
///////////////////////////
// Problem occurs here:
key: UniqueKey(), // key: key,
// Keyboard cannot be used when the above keys are used (i.e. UniqueKey() or from baseclass).
// However, state changes due to pressing IconButton do not update text value without using UniqueKey().
// Using the baseclass key also results in closing the keyboard and IconButtons do not update TextFormField
initialValue: NumberFormat("#.#").format(state.value),
keyboardType: TextInputType.numberWithOptions(
decimal: (scale > 0)),
textAlign: TextAlign.center,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(
scale > 0 ? r'[0-9]+[,.]{0,1}[0-9]*' : r'[0-9]')),
TextInputFormatter.withFunction(
(oldValue, newValue) => newValue.copyWith(
text: newValue.text.replaceAll('.', ','),
),
),
],
style: textStyle,
autovalidateMode: AutovalidateMode.onUserInteraction,
onSaved: (value) {
if (value != null && value != "") {
NumberFormat().parse(value).toDouble();
}
},
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
state.didChange((state.value != null)
? state.value! + 1
: initialValue);
},
),
],
),
state.hasError
? Text(
state.errorText ?? "",
style: const TextStyle(color: Colors.red),
)
: Container()
],
);
},
);
}
Related
On my flutter webapp I am trying to show the password if the user chose to show it. If I set my bool value to false it start by showing the normal text and then if I press the button to show or hide the password it will obscure the text but it will not un-obscure the text. I added a print function in one of my try's and it does change the bool value from false to true and back to false. I have it it a setState but no luck. I tryied many different examples online but can not get it to work with the web app.
The code
#override
Widget build(BuildContext context) {
c_width = MediaQuery.of(context).size.width*0.8;
return Scaffold(
appBar: AppBar(title: Text("App"),
),
body: signUpWidget(widget.signUpValue),
);
}
Widget signUpWidget(String? rl) {
switch(rl) {
case 'User': {
return SingleChildScrollView(
child: Container (
width: c_width,
padding: const EdgeInsets.all(16.0),
child: Column(
child: Column(
children: <Widget>[
TextFormField(
obscureText: isObscure,
decoration: InputDecoration(
suffix: TextButton(
child: isPasswordObscure
? Text(
'Show',
style: TextStyle(color: Colors.grey),
)
: Text(
'Hide',
style: TextStyle(color: Colors.grey),
),
onPressed: () {
setState(() {
isObscure = !isObscure;
isPasswordObscure = !isObscure;
});
},
),
),
),
Like I say it changes the text to hide and show and if I print the bool value it changes from true to false, but it does not unObscure the text in the field to show the password. Is it something to do with web? or am I missing something?
Thank you.
Actually you dont need to have two bool,
TextFormField(
obscureText: isObscure,
decoration: InputDecoration(
suffix: TextButton(
child: isObscure //< using
? Text(
'Show',
style: TextStyle(color: Colors.grey),
)
: Text(
'Hide',
style: TextStyle(color: Colors.grey),
),
onPressed: () {
setState(() {
isObscure = !isObscure;
// isPasswordObscure = !isObscure; // here it reverse the variable isPasswordObscure
});
},
),
),
)
It might be occurring due to this problem in setState((){}).
setState(() {
// assume isObscure is true initially
isObscure = !isObscure;
// now isObscure is false
isPasswordObscure = !isObscure;
// now isPasswordObscure is true, because isObscure's value has already changed.
});
To solve this, first save isObscure to a temporary variable.
setState(() {
var temp = isObscure;
isObscure = !temp;
isPasswordObscure = !temp;
});
Or,
Entirely avoid using two bools.
setState(() {
isObscure = !isObscure;
});
It seems like this occurs only in web.
Follow these steps to fix this,
https://github.com/flutter/flutter/issues/83695#issuecomment-1083537482
Or
Upgrade to the latest Flutter version.
I have used a package group_button 4.2.1 but once i select the textfields the radio buttons deselect and i have to select again, i have tried using the controller property of the Widget but i didn't get it to work.
I was thinking if i can make a container from scratch that is a radio button and can retain the value once i finish filling the form to be submitted to my firestore database.
You can use List of button text and keep tract of selectedIndex.
Run on dartPad
int? _selectedValueIndex;
List<String> buttonText = ["ForSale", "For rent"];
Widget button({required String text, required int index}) {
return InkWell(
splashColor: Colors.cyanAccent,
onTap: () {
setState(() {
_selectedValueIndex = index;
});
},
child: Container(
padding: const EdgeInsets.all(12),
color: index == _selectedValueIndex ? Colors.blue : Colors.white,
child: Text(
text,
style: TextStyle(
color: index == _selectedValueIndex ? Colors.white : Colors.black,
),
),
),
);
}
Inside build method to use this,
Row(
children: [
...List.generate(
buttonText.length,
(index) => button(
index: index,
text: buttonText[index],
),
)
],
),
I am fairly new at Flutter so I might have gotten things wrong but ... I can't get my head around this issue.
I have ready many posts on how allow multi-line text for TextField / TextFormField.
I have tried setting maxLines to null or to a big number, to enforce max lines (or not), but nothing works !
How come something as simple won't work ?
// in a Scaffold of a StatefulWidget
body: Padding(
padding: const EdgeInsets.all(50.0),
child: TextField(
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: 5,
),
I have tried on two different real Android devices and hitting the ENTER key does nothing. Unfortunately, I can't test the behavior on iOS as I don't have a Mac.
EDIT 1
Following solution offered by copsonroad, the following code
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.event.shortName == null || widget.event.shortName.isEmpty
? AppLocalizations.of(context).eventEditorTitleNewEvent
: AppLocalizations.of(context)
.eventEditorTitleEditEvent(widget.event.shortName)),
actions: <Widget>[
if (_canSave == true) ...[
IconButton(
icon: Icon(Icons.done),
onPressed: () => _saveEventAndPop(),
)
],
]),
body: Center(
child: Padding(
padding: const EdgeInsets.all(50),
child: SizedBox(
height: 5 * 50.0,
child: TextField(
decoration: InputDecoration(hintText: "Enter a message"),
keyboardType: TextInputType.multiline,
maxLines: 5,
),
),
),
),
);
}
Produce the following result:
Screenshot:
#override
Widget build(BuildContext context) {
var maxLines = 5;
return Scaffold(
appBar: AppBar(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(50),
child: SizedBox(
height: maxLines * 50.0,
child: TextField(
decoration: InputDecoration(hintText: "Enter a message"),
keyboardType: TextInputType.multiline,
maxLines: maxLines,
),
),
),
),
);
}
Edit:
Testing it on Android:
There is actually a bug with TextInputType.multiline. It simply doesn't work with some custom keyboards (i.e. SwiftKey). There is an issue in flutter github where I described my workaround.
First of all, I've wrapped my TextFormField with RawKeyboardListener like this:
RawKeyboardListener(
focusNode: FocusNode(),
autofocus: false,
onKey: (rawKeyEvent) {
final isFormSkippedEnterEvent = rawKeyEvent is RawKeyDownEvent &&
rawKeyEvent.isKeyPressed(LogicalKeyboardKey.enter);
final needToInsertNewLine = isFormSkippedEnterEvent &&
keyboardType == TextInputType.multiline;
if (needToInsertNewLine) {
TextEditingControllerHelper.insertText(controller, '\n');
}
},
child: TextFormField(...)
...
When Gboard is used, RawKeyboardListener doesn't receive Enter event at all, it's sent directly to the text field. But when using for example SwiftKey, instead of inserting new line to the text, RawKeyboardListener receives an event RawKeyDownEvent#a5131(logicalKey: LogicalKeyboardKey#70028(keyId: "0x100070028", keyLabel: "Enter", debugName: "Enter"). I check if I need to insert a new line (no need for this if TextFormField is configured to be single line) and access TextEditingController to alter user's input. All the magic happens inside TextEditingControllerHelper.insertText():
class TextEditingControllerHelper {
static insertText(TextEditingController controller, String textToInsert) {
final selection = controller.selection;
final cursorPosition = selection.base.offset;
if (cursorPosition < 0) {
controller.text += textToInsert;
return;
}
final text = controller.text;
final newText =
text.replaceRange(selection.start, selection.end, textToInsert);
controller.value = controller.value.copyWith(
text: newText,
selection: TextSelection.collapsed(
offset: selection.baseOffset + textToInsert.length,
),
);
}
}
What I do is check cursor/ selection position. In case of an empty field I just attach a newLine (this helper can work with other inputs, i.e. emojis) and exit. Otherwise I insert a new line on place of existing selection (if there is no selection, text is just inserted where cursor is placed) and move cursor to be after the inserted text. Works fine in my tests (Pixel 3a with Gboard/SwiftKey, Galaxy S8 with Samsung keyboard, iPhone simulator).
I create a profile page and i have 4 textformfield. I want to on tap icon activate textformfield and focus at the same time. Now I need tap twice on icon and first activated field, secondly focused.
How to solve it?
My code:
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
FocusNode myFocusNode;
bool isEnable = false;
#override
void initState() {
super.initState();
myFocusNode = FocusNode();
}
#override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.fromLTRB(40.0, 50.0, 20.0, 0.0),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Flexible(
child: TextFormField(
enabled: isEnable,
focusNode: myFocusNode,
),
),
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
setState(() {
isEnable = true;
FocusScope.of(context).requestFocus(myFocusNode);
});
})
],
),
You should use autofocus: isEnable instead.
just do like below in your ontap
setState(() {
if(isEnable)
Future.delayed(const Duration(milliseconds: 10), ()
{FocusScope.of(context).requestFocus(myFocusNode);
});
isEnable = true;
});
in first time isEnable is false so focusing not call and just enabling work and in other times get focus too.
you can't focus widget until disabled and when you enabling widget. when you do focusing and enabling at same time in ui tread it's try focusing before enabling because of their rendering time.if you post some delay to focusing the problem get solving.
Try using readOnly instead of enabled in TextFormField
I faced similar issue when I had multiple TextFields to enable kinda PIN input. And some of that had to be dynamically enabled and disabled plus prevent users from entering value in the next field while they haven't finished the previous one. I've tried a lot of approaches and focusing field after some delay was not a way to go because I wanted the keyboard to always be available while entering. So I've took a crazy path and solved this next way:
onTap: () => _focusNodes[_currentLetterIndex].requestFocus()
where _focusNodes are for each letter and _currentLetterIndex is calculated programmatically during input (when finished letter 0 -> current becomes 1 and so on). As the result when user tried to tap on next field - it was automatically refocused to the current one which behaves like the next field is disabled.
An example of the full text field looks like this (don't pay attention to decorations etc.)
TextField(
key: ValueKey(index),
controller: _editingControllers[index],
onTap: () => _focusNodes[_currentLetterIndex].requestFocus(),
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
showCursor: false,
maxLength: 2,
enableInteractiveSelection: false,
autocorrect: false,
enableSuggestions: false,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.2,
decoration: TextDecoration.none),
decoration: InputDecoration(
fillColor: !_wordCompleted &&
!correct &&
_editingControllers[index].text.isNotEmpty
? const Color(0xFFFFEEF0)
: Colors.white,
filled: !correct,
counterText: "",
border: defaultInputBorder,
focusedBorder: !correct &&
!_wordCompleted &&
_editingControllers[index].text.isNotEmpty
? incorrectInputBorder
: focusedInputBorder,
enabledBorder: _wordCompleted
? focusedInputBorder
: correct
? correctInputBorder
: defaultInputBorder,
errorBorder: defaultInputBorder,
disabledBorder: _wordCompleted
? focusedInputBorder
: correct
? correctInputBorder
: defaultInputBorder,
contentPadding: const EdgeInsets.only(left: 2)),
),
Solved:
please check my own answer..It's working well..but Is that proper way to fix like this issue?
This is the registration details page. First-row display text-form-field and then dropdown and etc. User typing value and select dropdown and click button information value are showing. But user select dropdown value and then typing information and click the button, information value is null.
AND ALSO typing value on textFormField and click button first time value is null, then click again button value is showing. Why is that?
I added print('textfield is $details'), in onPressed() method.
scenario 1: typing information and click button value is null
scenarion 2: typing information and select dropdown/tap on screen and
click button value is showing
class _overview3 extends State<overview3> {
final detailsController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: overviewAppBar(
detailsController.text),
body: Scaffold(
body: Container(
child: Container(
child: Scaffold(
body: ListView(
children: <Widget>[
TextFormField(
controller: detailsController,
maxLength: 100,
maxLines: 2,
keyboardType: TextInputType.multiline,
validator: (value) {
if (value.length > 3) {
return '100 values';
}
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(20.0),
hintText: AppTranslations.of(context).text("additional_details"),
),
),
],
),
)))
));
}
in my OverviewAppBar method,
overviewAppBar(String details) =>
AppBar(
brightness: Brightness.dark,
backgroundColor: const Color(0xFFE27023),
centerTitle: true,
title: appBarIcon(context),
actions: <Widget>[
IconButton(
icon: _isButtonDisabled
? Icon(
Icons.check,
color: Colors.white30,
)
: Icon(Icons.check),
onPressed: () => _isButtonDisabled
? null
: globals.isButtonClick
? null
:{FocusScope.of(context).requestFocus(FocusNode()),
print('textfield is $details')
}
)
]
);
global.dart file
final globalDetailsController = TextEditingController();
in textformfield
TextFormField(
controller: global.globalDetailsController
)
in OverviewAppBar method in onPressed()
print('textfield is ${global.globalDetailsController}')