Flutter TextField TextFieldController setState - position of cursor changes - flutter

I recently wrote a test program that I needed that is essentially a CRUD program. I needed to handle this differently to other similar programs that I have written, because I normally use a stateful FAB widget, and don't have to setState() to enable and disable the FAB. In this test program I didn't want to use the custom FAB, and used the standard FAB. I found that whenever I had to enable or disable the FAB because of a change to a TextField, that this required a setState(), and after the build, the cursor for the TextField that was being edited had repositioned. I don't know why that happens, because I had not recreated the Widgets. The only solution that I could come up with to handle that issue was fairly messy and required saving the Widget position in the List of TextField and also save the Selection, and then after the build resetting the Selection to the saved Selection.
What I need to achieve is for the FAB to be only enabled when data has changed. Obviously this can vary with every key entry.
I presume I'm not handling this in the optimal way. How is this handled so that the cursor position remains as it was prior to the build?
----- Have Now Added Code below ----
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
//=====================================================================================
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test Widgets',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(title: 'Test Widgets'),
);
}
}
//=====================================================================================
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
//=====================================================================================
class _HomePageState extends State<HomePage> {
bool _tfDataHasChanged = false;
bool _tfInitialized = false;
bool _tfSaveSelection = false;
int _iNdxWidgetChanged = -1;
List<String> _lsOldData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<String> _lsNewData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<TextField> _lwTextFields;
TextSelection _wTextSelection;
//-------------------------------------------------------------------------------------
#override
void dispose() {
for (int iNdxWidget = 0;
_lwTextFields != null && iNdxWidget < _lwTextFields.length;
iNdxWidget++) {
_lwTextFields[iNdxWidget].focusNode.removeListener(() {
_fnFocusChanged();
});
_lwTextFields[iNdxWidget]?.controller?.dispose();
_lwTextFields[iNdxWidget]?.focusNode?.dispose();
}
super.dispose();
}
//-------------------------------------------------------------------------------------
#override
Widget build(BuildContext context) {
_tfInitialized = false;
SchedulerBinding.instance.addPostFrameCallback((_) => _fnOnBuildComplete());
if (_lwTextFields == null) {
_fnCreateAllWidgets();
}
List<Widget> lwDisplay = _fnCreateDisplay();
return Scaffold(
appBar: AppBar(
flexibleSpace: SafeArea(
child: _fnCreateAppBarWidgets(),
)),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: lwDisplay,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _tfDataHasChanged ? _fnUpdateData : null,
tooltip: 'Update',
backgroundColor: _tfDataHasChanged ? Colors.blue : Colors.grey,
child: Icon(Icons.done),
),
);
}
//-------------------------------------------------------------------------------------
_fnOnBuildComplete() {
_tfInitialized = true;
if (_tfSaveSelection && _iNdxWidgetChanged >= 0) {
_lwTextFields[_iNdxWidgetChanged].controller.selection = _wTextSelection;
}
}
//-------------------------------------------------------------------------------------
void _fnCreateAllWidgets() {
_lwTextFields = List(_lsNewData.length);
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_fnCreateTextField(iNdxWidget);
}
}
//-------------------------------------------------------------------------------------
void _fnCreateTextField(int iNdxWidget) {
TextEditingController wController = TextEditingController();
FocusNode wFocusNode = FocusNode();
wFocusNode.addListener(() => _fnFocusChanged());
_lwTextFields[iNdxWidget] = TextField(
autofocus: false, //(iNdxWidget == 0),
autocorrect: false,
enabled: true,
keyboardType: TextInputType.text,
maxLength: 25,
controller: wController,
focusNode: wFocusNode,
textInputAction: TextInputAction.next /* TYPE OF ACTION KEY */,
onSubmitted: ((v) => _fnSetNextFocus(iNdxWidget)),
onChanged: (text) => _fnTextListener(iNdxWidget, text),
decoration: _fnCreateInputDecoration(
'Text Field Number ${iNdxWidget + 1}', 'Enter Data'),
style: _fnCreateWidgetTextStyle(Colors.blue[700]),
);
}
//-------------------------------------------------------------------------------------
_fnTextListener(int iNdxWidget, String sText) {
if (_tfInitialized) {
_lsNewData[iNdxWidget] = sText;
_fnCheckIfDataHasChanged(
iNdxWidget) /* ENABLE OR DISABLE SUBMIT BUTTON */;
}
}
//-------------------------------------------------------------------------------------
_fnSetNextFocus(int iNdxWidget) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_lwTextFields[iNdxWidget].focusNode.unfocus();
if (iNdxWidget + 1 < _lwTextFields.length) {
_lwTextFields[iNdxWidget + 1]?.focusNode?.requestFocus();
}
}
}
//-------------------------------------------------------------------------------------
InputDecoration _fnCreateInputDecoration(String sHeading, String sHint) {
return InputDecoration(
labelText: sHeading,
hintText: sHint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(20.0)),
);
}
//-------------------------------------------------------------------------------------
TextStyle _fnCreateWidgetTextStyle(Color color) {
return TextStyle(
fontSize: 14.0,
color: color,
);
}
//-------------------------------------------------------------------------------------
List<Widget> _fnCreateDisplay() {
List<Widget> lwDisplay = List((_lwTextFields.length * 2) + 2);
lwDisplay[0] = SizedBox(height: 10);
int iNdxDisplay = 1;
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
lwDisplay[iNdxDisplay++] = _lwTextFields[iNdxWidget];
lwDisplay[iNdxDisplay++] =
SizedBox(height: iNdxDisplay < lwDisplay.length - 2 ? 10 : 80);
}
lwDisplay[lwDisplay.length - 1] = Divider(color: Colors.black, height: 2);
return lwDisplay;
}
//-------------------------------------------------------------------------------------
_fnUpdateData() {
for (int iNdxWidget = 0; iNdxWidget < _lsNewData.length; iNdxWidget++) {
if (_lsNewData[iNdxWidget] != _lsOldData[iNdxWidget]) {
_lsOldData[iNdxWidget] = _lsNewData[iNdxWidget];
}
}
_fnCheckIfDataHasChanged(-1);
}
//-------------------------------------------------------------------------------------
_fnCheckIfDataHasChanged(int iNdxWidgetChanged) {
bool tfChanged = false /* INIT */;
for (int iNdxWidgetTest = 0;
!tfChanged && iNdxWidgetTest < _lsNewData.length;
iNdxWidgetTest++) {
tfChanged = _lsNewData[iNdxWidgetTest] != _lsOldData[iNdxWidgetTest];
}
if (iNdxWidgetChanged >= 0) {
_iNdxWidgetChanged = iNdxWidgetChanged;
_wTextSelection = _lwTextFields[iNdxWidgetChanged].controller.selection;
}
if (tfChanged != _tfDataHasChanged) {
setState(() => _tfDataHasChanged = tfChanged) /* WE NEED TO ENABLE FAB */;
}
}
//-------------------------------------------------------------------------------------
Row _fnCreateAppBarWidgets() {
IconData wIconData =
_tfSaveSelection ? Icons.check_box : Icons.check_box_outline_blank;
Color wColor = _tfSaveSelection ? Colors.blue[900] : Colors.grey[600];
IconButton wIconButton = IconButton(
icon: Icon(wIconData),
color: wColor,
onPressed: _fnCheckboxChanged,
iconSize: 40);
return Row(children: <Widget>[
SizedBox(width: 10),
Text('Save\nSelection', textAlign: TextAlign.center),
wIconButton,
SizedBox(width: 30),
Text('Test TextField')
]);
}
//-------------------------------------------------------------------------------------
_fnFocusChanged() {
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_iNdxWidgetChanged = iNdxWidget;
_wTextSelection = _lwTextFields[iNdxWidget].controller.selection;
return;
}
}
}
//-------------------------------------------------------------------------------------
void _fnCheckboxChanged() {
_tfSaveSelection = !_tfSaveSelection;
if (!_tfSaveSelection) {
_iNdxWidgetChanged = -1;
}
setState(() {});
}
}
-------- Have added key to TextField, but issue persists ---------
key: ValueKey<int>(iNdxWidget),

My bug - as posted by #pskink
My excuse - I normally use a stateful FAB, so I don't normally encounter this.
Answer:
so change this line:
TextEditingController wController = TextEditingController(text: _lsNewData[iNdxWidget]);
and remove this one
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
– pskink Feb 23 at 7:33

I hope these functions might help you
void updateText(String text) {
if (text != null) {
this.text = _applyMask(mask, text);
} else {
this.text = '';
}
_lastUpdatedText = this.text;
}
void updateMask(String mask, {bool moveCursorToEnd = true}) {
this.mask = mask;
updateText(text);
if (moveCursorToEnd) {
this.moveCursorToEnd();
}
}
void moveCursorToEnd() {
final String text = _lastUpdatedText;
selection =
TextSelection.fromPosition(TextPosition(offset: (text ?? '').length));
}

Related

How to automatically select all items above the selected one flutter

I have a custom list view with selectable items.And I am trying to select all items automatically present above the one I selected. For Ex: Suppose there is 10 items in the list view and i selected 5th then it should select all the items available above 5th. i.e(1,2,3,4) and when i deselect 3rd item 1,2,3 items should deselected automatically
return CheckboxListTile(
activeColor: const Color.fromARGB(
255, 243, 243, 243),
checkColor: UIGuide.light_Purple,
selectedTileColor:
UIGuide.light_Purple,
value: value.selecteCategorys
.contains(value.feeList[index]
.installmentName ??
'--'),
onChanged: (bool? selected) async {
value.onFeeSelected(
selected!,
value.feeList[index]
.installmentName,
index,
value.feeList[index].netDue);
},
title: Text(
value.feeList[index].netDue ==
null
? '--'
: value.feeList[index].netDue
.toString(),
textAlign: TextAlign.end,
),
secondary: Text(
value.feeList[index]
.installmentName ??
'--',
),
);
do something like this :
1 - get index of selected item
2 - in the callback fun of checkbox do
let say we have list of items named by items
List<Item> items = [];
foo() {
final upperlist = items.getRange(0, index).toList();
upperlist.forEach((item) {item.selected =true });
items.replaceRange(0, index, upperlist);
setState((){});
}
Note, this example isn't perfect, but it's a working example that can get you thinking, as I don't know the bigger picture
Here's my approach:
get the widget and index of the currently selected value using .indexOf()
loop over all the widgets until the previously gotten index
for (var i = 0; i < _data.indexOf(item); i++) {
_data[i].isChecked = value!;
}
Code example
create a class called CheckBoxModel:
class CheckBoxModel {
bool isChecked = false;
String text = "";
CheckBoxModel({required this.isChecked, required this.text});
}
and then, generated 30 widgets:
final _data = List.generate(
30, (index) => CheckBoxModel(isChecked: false, text: "Item $index"));
and used it correspondingly:
Column(
children: [
for (var item in _data)
CheckboxListTile(
value: item.isChecked,
onChanged: (value) {
setState(() {
for (var i = 0; i < _data.indexOf(item); i++) {
_data[i].isChecked = value!;
}
});
},
title: Text(item.text),
),
],
)
Here's a complete runnable snipppet:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class CheckBoxModel {
bool isChecked = false;
String text = "";
CheckBoxModel({required this.isChecked, required this.text});
}
final _data = List.generate(
30, (index) => CheckBoxModel(isChecked: false, text: "Item $index"));
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Testing(),
),
),
);
}
}
class Testing extends StatefulWidget {
const Testing({Key? key}) : super(key: key);
#override
State<Testing> createState() => _TestingState();
}
class _TestingState extends State<Testing> {
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
for (var item in _data)
CheckboxListTile(
value: item.isChecked,
onChanged: (value) {
setState(() {
for (var i = 0; i < _data.indexOf(item); i++) {
_data[i].isChecked = value!;
}
});
},
title: Text(item.text),
),
],
),
);
}
}

How to limit text field with a value in flutter?

I want to limit the value that user input not to over a expected value in flutter text field.
Example,
If there is a value come from API is 10 and want to limit the input not over 10, if client type 11 or something over 10, want to show alert or make user not to type.
How to control this?
TextFormField(
style: TextStyle(fontSize: 20),
onChanged: (value) {
if (value != "") {
int _checkValue = int.parse(value);
if (_checkValue >
Provider.of<SaleProvider>(context, listen: false)
.remainNewQuantity(
this.currentProductItemSelected.id)) {
return 'error';
} else {
setState(() {
this.qty = int.parse(value);
updateByQty();
});
}
} else {
setState(() {
});
}
},
),
This is my trying, but can't do that I want.
Please check below method. I think this will resolve your issue. If still not work, please let me know
Widget getTextField({required int maxValue}) {
return TextFormField(
controller: _textController,
keyboardType: TextInputType.number,
onChanged: (text) {
if (int.parse(text) > maxValue) {
// show popup here.
_textController.text = validText;
_textController.selection = TextSelection.fromPosition(TextPosition(offset: _textController.text.length));
}else{
validText = text;
}
},
);
}
As an exercise for my own learning I gave it a go and came up with the following approach; creating a bespoke widget which admittedly looks like a lot of code for something so simple... but it appears to work as expected I think and one could modify, expand and integrate it with other elements in various ways.
Usage: const MaxIntField(max: 100),
Implementation:
class MaxIntField extends StatefulWidget {
const MaxIntField({Key? key, this.max = 1}) : super(key: key);
final int max;
#override
State<MaxIntField> createState() => _MaxIntFieldState();
}
class _MaxIntFieldState extends State<MaxIntField> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.value.copyWith(text: '0');
_controller.addListener(() {
if (_controller.text.isNotEmpty && _controller.text != '0') {
int intVal = int.parse(_controller.text);
if (intVal > widget.max) {
setState(() {
_controller.value =
_controller.value.copyWith(text: widget.max.toString());
_showMyDialog();
});
} else if (_controller.text != intVal.toString()) {
//remove leading '0'
setState(() {
_controller.value =
_controller.value.copyWith(text: intVal.toString());
});
}
}
});
}
// assuming using Material
_showMyDialog() async {
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('AlertDialog Title'),
content: Text('This field is limited to ${widget.max}'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
);
}
}

Listview Ui won't update while applying filter /search

I have a listview to which am applying a filter , the filter works but the UI won't update the list is unchanged .
this is my implementation
class QrqcOnlineListView extends StatefulWidget {
#override
_QrqcOfflineListViewState createState() => _QrqcOfflineListViewState();
}
List<Qrqc> filteredQrqc = [];
List<Qrqc>? qrqcList = [];
class _QrqcOfflineListViewState extends State<QrqcOnlineListView> {
List<Qrqc>? myList;
String? label;
Future<List<Qrqc>?> getQrqcData() => SharedPreferenceMyQrqc().getListQrqc();
List<Qrqc> filteredQrqc = [];
final _controller = TextEditingController();
String? _searchText;
List<TypeSettings> listTypes = [];
#override
void initState() {
Provider.of<MyQrqcListViewModel>(context, listen: false).fetchMyQrqc();
super.initState();
fetchTypes();
}
fetchTypes() async {
listTypes = (await SettingsViewModel().fetchTypes());
}
#override
Widget build(BuildContext context) {
var myQrqcListViewModel = Provider.of<MyQrqcListViewModel>(context);
myList = myQrqcListViewModel.articlesList;
List<Qrqc>? qrqcList = myList;
filteredQrqc = myList!;
List<Qrqc> listFilter = List.from(qrqcList!);
QrqcDetails? result;
String? type;
void updateList(String value) {
setState(() {
listFilter = qrqcList
.where((element) =>
element.title!.toLowerCase().contains(value.toLowerCase()))
.toList();
});
}
String? setTypeLabel() {
for (int j = 0; j < listFilter.length; j++) {
for (int i = 0; i < listTypes.length; i++) {
if (listTypes[i].id == listFilter[j].typeID) {
listFilter[j].typeName = listTypes[i].label;
}
}
}
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(
hintText: "Search",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(4.0),
),
),
),
onChanged: (value) => updateList(value),
),
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: listFilter.length,
itemBuilder: (BuildContext context, int index) {
String? backgroundImage;
String? _setImage() {
setTypeLabel();
if (listFilter[index].typeName == "Delivery") {
backgroundImage = "assets/icons/icon_delivery.png";
} else if (listFilter[index].typeName == "Security") {
backgroundImage = "assets/icons/icon_security.png";
} else if (listFilter[index].typeName == "Quality") {
backgroundImage = "assets/icons/quality.png";
} else if (listFilter[index].typeName == "Cost") {
backgroundImage = "assets/icons/Cost.png";
} else if (listFilter[index].typeName == "People") {
backgroundImage = "assets/icons/people.png";
} else {
backgroundImage = "assets/icons/unknown.png";
}
// print("list types: $qrqcList[index].typeName");
// print("_mTitle: $backgroundImage");
return backgroundImage;
}
return Column(
children: [
QrqcBody(
child: QrqcCard(
child: QrqcCardBody(
color: Colors.orange,
text: listFilter[index].status,
leading: QrqcCardLeaing(imgPath: _setImage()),
trailing: QrqcCardtrailing(
text: listFilter[index].progress.toString(),
percent: listFilter[index].progress.toString(),
),
title: listFilter[index].id.toString(),
subtitle: listFilter[index].title,
chlidren: [
QrqcDetailsCardFirstRow(
product: listFilter[index].productName ?? 'inconnu',
role: listFilter[index].role ?? "inconnu",
),
const SizedBox(height: 10),
QrqcDetailsCardSecondRow(
perim: listFilter[index].perimeterName ?? "inconnu",
date: convertDateTimeDisplay(
listFilter[index].createdAt!),
),
const SizedBox(height: 10),
],
)),
),
],
);
}),
),
],
);
}
}
I don't know what's wrong especially that the filter is working and i got no errors only the keyboard events in the stack trace , if anyone can help i'd be grateful , i've been stuck for a while now
Your issue is that your list is being re-initialized / reset every time in the build method; while you're filter the list correctly inside the setState, when the build method re-executes upon calling setState, you're resetting it again when you do this:
// upon every rebuild, the listFilter gets reset to the original value
List<Qrqc> listFilter = List.from(qrqcList!);
What you could is:
Use the existing property called ** _searchText** and store the value being provided as input by the TextField widget, that when it's empty, then you fetch the whole list, otherwise skip that logic, that way your listFilter remains filtered on the next widget rebuild, kind of like:
In your *updateList method:
void updateList(String value) {
// store the value being provided as input to the filter
// in the property called _searchText
_searchText = value;
setState(() {
listFilter = qrqcList
.where((element) =>
element.title!.toLowerCase().contains(_searchText.toLowerCase()))
.toList();
});
}
Inside the build method:
// check if its empty, that means there's no filter being applied
if (_searchText.isEmpty) {
List<Qrqc> listFilter = List.from(qrqcList!);
}

Flutter Search bar filter issues

I have a project that I have been working on and have been stumped on this for the last month and can't seem to figure out how to solve this. I have a screen that shows a list of contacts and a search bar that is used to show specific contacts. I have been trying to add a filter feature where the use clicks on filters and chooses a couple options that will show. The way the button widget works I couldn't get it to be in the same class so it's in another file and being used. Here is the code, I am still learning Flutter so if you have an recommendations to fix other bits of the code I would love to hear it.
Here is my first file with the screen that include the search bar and the the major chunk of code.
import 'package:myapp/widgets/cards.dart';
import 'package:flutter/material.dart';
import 'package:myapp/widgets/icon_buttons.dart';
class ResourceScreen extends StatefulWidget {
const ResourceScreen({Key? key}) : super(key: key);
#override
_ResourceScreen createState() => _ResourceScreen();
}
class _ResourceScreen extends State<ResourceScreen> {
bool filtered = false;
final TextEditingController _filter = TextEditingController();
String _searchText = "";
Widget customSearchBar = const Text('Resources');
Icon customIcon = const Icon(Icons.search);
List filterCardInformation = [],
tempList = [],
filterButtons = [],
cardInformation = phoneList;
void _getCards() async {
setState(() {
filterCardInformation = cardInformation;
});
}
//TODO There seems to be some lag when the list goes from nothing to the whole list. Not sure what is impacting this or how hard it could be on the users experience as list grows
_ResourceScreen() {
_filter.addListener(() {
filterCardInformation =
cardInformation; // Might need in every else statement, not sure if there is a memory leak with it being here
//Had "tempList = []" only here and caused the program to run out of memory. This is why I believe this could be an issue
if (_filter.text.isEmpty) {
if (filtered) {
setState(() {
_searchText = "";
tempList = [];
for (int i = 0; i < filterCardInformation.length; i++) {
if (filterPhrase.contains(filterCardInformation[i].metadata)) {
tempList.add(filterCardInformation[i]);
}
}
filterCardInformation = tempList;
});
} else {
setState(() {
_searchText = "";
});
}
} else {
if (filtered) {
setState(() {
_searchText = _filter.text;
tempList = [];
for (int i = 0; i < filterCardInformation.length; i++) {
if (filterPhrase.contains(filterCardInformation[i].metadata)) {
tempList.add(filterCardInformation[i]);
}
}
filterCardInformation = tempList;
});
} else {
setState(() {
_searchText = _filter.text;
});
}
}
});
}
//This is where the list is built as input comes in from the keyboard
Widget _buildList(context) {
if (_searchText.isNotEmpty && !filtered) {
tempList = [];
for (int i = 0; i < filterCardInformation.length; i++) {
if ((filterCardInformation[i].title.toString())
.toLowerCase()
.contains(_searchText.toLowerCase())) {
tempList.add(filterCardInformation[i]);
}
}
filterCardInformation = tempList;
} else if (_searchText.isNotEmpty && filtered) {
tempList = [];
for (int i = 0; i < filterCardInformation.length; i++) {
if ((filterCardInformation[i].title.toString())
.toLowerCase()
.contains(_searchText.toLowerCase()) &&
filterPhrase.contains(filterCardInformation[i].metadata)) {
tempList.add(filterCardInformation[i]);
}
}
filterCardInformation = tempList;
}
return ListView.builder(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
itemCount: filterCardInformation.length,
itemBuilder: (BuildContext context, int index) {
return MyExpandingCard(
title: filterCardInformation[index].title,
information: filterCardInformation[index].information,
phoneList: filterCardInformation[index].phoneList,
website: filterCardInformation[index].website,
contactList: filterCardInformation[index].contactList,
);
},
);
}
#override
void initState() {
_getCards();
super.initState();
}
//Trigger filter list to show from the bottom
Widget filterButton(double height) {
return ElevatedButton.icon(
onPressed: () => {filterBottomSheet(height)},
//TODO Change out this icon for something different
icon: const Icon(Icons.filter),
label: const Text("Filter"),
);
}
//Row with buttons for bottomSheet
Widget doubleBottomButtonRow(String dataOne, dataTwo) {
Icon firstPersonalIcon = const Icon(Icons.add),
secondPersonalIcon = const Icon(Icons.add);
void _changeFirstIcon() {
setState(() {
if (firstPersonalIcon.icon == Icons.add) {
firstPersonalIcon = const Icon(Icons.check);
} else {
firstPersonalIcon = const Icon(Icons.add);
}
});
}
void _changeSecondIcon() {
setState(() {
if (secondPersonalIcon.icon == Icons.add) {
secondPersonalIcon = const Icon(Icons.check);
} else {
secondPersonalIcon = const Icon(Icons.add);
}
});
}
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton.icon(
//TODO issue here with icon not changing on click. I am not sure exactly why, I just need to figure out how to trigger feedback from when button in different file is used
onPressed: () {
_changeFirstIcon();
},
style: ElevatedButton.styleFrom(shape: const StadiumBorder()),
label: Text(dataOne),
icon: firstPersonalIcon),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton.icon(
onPressed: () {
_changeSecondIcon();
},
style: ElevatedButton.styleFrom(shape: const StadiumBorder()),
label: Text(dataTwo),
icon: secondPersonalIcon),
),
],
);
}
Widget singleBottomButtonRow(String dataOne) {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: IButton(firstName: dataOne)),
],
);
}
//BottomSheet filter
Future filterBottomSheet(double height) {
return showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (BuildContext context) {
return SizedBox(
height: height,
child: SingleChildScrollView(
child: Column(
children: [
doubleBottomButtonRow(
"filter 1", "filter 2"),
],
),
));
});
}
//TODO Design this
//Widget that keeps track of how many visible options are shown
#override
Widget build(BuildContext context) {
//Grab size of screan for the bottom sheet
Size size = MediaQuery.of(context).size;
double height = size.height;
return Scaffold(
appBar: AppBar(
backgroundColor: const Color(0xff1c1d4b),
title: customSearchBar,
automaticallyImplyLeading: true,
actions: [
IconButton(
onPressed: () {
setState(() {
if (customIcon.icon == Icons.search) {
customIcon = const Icon(Icons.cancel);
//search bar
customSearchBar = ListTile(
leading: const Icon(
Icons.search,
color: Colors.white,
size: 28,
),
title: TextField(
controller: _filter,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Type in search...',
hintStyle:
TextStyle(color: Colors.white, fontSize: 18),
border: InputBorder.none,
),
style: const TextStyle(color: Colors.white),
),
);
//Reset the variables
} else {
customIcon = const Icon(Icons.search);
customSearchBar = const Text('Resources');
}
});
},
icon: customIcon)
],
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
Row(
children: [
filterButton(height * .30),
],
),
Expanded(
child: SingleChildScrollView(
physics: const ScrollPhysics(),
child: _buildList(context),
),
),
],
),
),
);
}
}
//TODO This is not working but what I was thinking of how the function would look
List filterPhrase = [];
void _addPhrase(String phrase) {
filterPhrase.add(phrase);
}
void _deletePhrase(String phrase) {
filterPhrase.remove(phrase);
}
List phoneList = [
const MyExpandingCard(
title: 'example 1',
phoneList: ['123456789'],
metadata: "filter 1",
),
const MyExpandingCard(
title: 'example 2',
phoneList: ['123456789'],
metadata: "filter 2",
),
];
And here is the other file that I am using for the button in the filter
import 'package:flutter/material.dart';
import 'package:myapp/secondary_screens/resource_screen.dart';
class IButton extends StatefulWidget {
final String firstName;
const IButton({Key? key, required this.firstName}) : super(key: key);
#override
State<IButton> createState() => _IButtonState();
}
class _IButtonState extends State<IButton> {
Icon personalIcon = const Icon(Icons.add);
void _changeIcon() {
setState(() {
if (personalIcon.icon == Icons.add) {
personalIcon = const Icon(Icons.check);
//add to filter list
} else {
personalIcon = const Icon(Icons.add);
//remove from filter list
}
});
}
#override
Widget build(BuildContext context) {
return ElevatedButton.icon(
onPressed: _changeIcon,
icon: personalIcon,
style: ElevatedButton.styleFrom(shape: const StadiumBorder()),
label: Text(widget.firstName),
);
}
}
If you have any resources to improve skills in flutter I would love to hear them. I am always looking to improve.
Thank you all

How to clear error message in TextFormField in Flutter

In my code I validate phone number. If phone number is incorrect - I show error message. But, when user starts to edit number I want to hide this error message.
I've found the solution with currentState.reset(), but it seems not the good one. I have to handle issues with saving text and cursor position. And I still have one small artifact. Normally when I press and hold backspace - it deletes symbols one by one. If I do it when error message is shown - then error message disappears and only one symbol is deleted.
Does anybody know the right solution for this case?
final TextEditingController controller = TextEditingController();
final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10, 15}\$");
bool isError = false;
TextSelection currentPosition;
return Column(
children: <Widget>[
Form(
key: _textKey,
child: TextFormField(
controller: controller,
validator: (str) {
isError = true;
if (str.isEmpty) {
return err_empty_field;
} else if (!_phoneRegex.hasMatch(str)) {
return err_invalid_phone;
}
isError = false;
},
),
onChanged: () {
if (controller.selection.start < 0 &&
controller.text.length > 0) {
TextSelection position =
controller.text.length > currentPosition.start
? currentPosition
: TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
controller.selection = position;
}
if (isError) {
isError = false;
currentPosition = controller.selection;
if (currentPosition.start > controller.text.length) {
currentPosition = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
}
String currentText = controller.text;
_textKey.currentState.reset();
controller.text = currentText;
controller.selection = currentPosition;
}
},
),
RaisedButton(
onPressed: () {
_textKey.currentState.validate();
},
child: Text(login),
)
],
);
EDIT (Nov 2020)
autovalidate was deprecated after v1.19.0.
Instead use autovalidateMode:
Form(
autovalidateMode: AutovalidateMode.onUserInteraction`.
...
)
Original post
here is a suitable solution to this problem.
You don't actually need to use onChanged or any tips causing side-effects, I solved it by creating a class property which is initialized to false:
bool _autovalidate = false;
The Form Widget has a named property autovalidate. You should pass it the previous boolean:
Form(
key: _textKey,
autovalidate: _autovalidate,
...
)
And in your Submit button onPressed() method, you should update the _autovalidate boolean to be true if the form is invalid, this will make the form to auto validate the TextFormField on every onChanged call:
RaisedButton(
onPressed: () {
if (_textKey.currentState.validate()) {
print('valid');
} else {
print('invalid');
setState(() => _autoValidate = true);
}
},
child: Text(login),
)
I hope it helped Somebody.
January 2021
...
AutovalidateMode _autoValidate = AutovalidateMode.disabled;
Form(
key: _textKey,
autovalidateMode: _autovalidate,
...
)
RaisedButton(
onPressed: () {
if (_textKey.currentState.validate()) {
print('valid');
} else {
print('invalid');
setState(() => _autoValidate = AutovalidateMode.always);
}
},
child: Text("login"),
)
The problem here is errorText is automatically managed by the validator field of the TextFormField. At the same time, the simple solution is to handle the errorText manually.
Step 1: Create
String field, _errorText initialised to null. The field will hold the error message that needs to be shown.
Boolean field, _error initialised to false. The filed is true if there is an error otherwise false.
Step 2:
Assign _errorText to TextFormField
Step 3 (Important):
Make sure that TextFormField validator returns a null value.
Handle the validation here and assign the proper error message to _errorText.
Update _error state correspondingly.
Step 4 (Important):
Reset _errorText and _error. This will remove the error from field soon as you start editing.
Step 5:
Trigger field validation in the onFieldSubmitted and manage your code flow...
import 'package:flutter/material.dart';
class WorkGround extends StatefulWidget {
#override
_WorkGroundState createState() => _WorkGroundState();
}
class _WorkGroundState extends State<WorkGround> {
final _formKey = GlobalKey<FormState>();
final _usernameFocusNode = FocusNode();
final _phoneNumberFocusNode = FocusNode();
/*
* Step 1.
* */
String _userNameErrorText;
bool _userNameError = false;
String _phoneNumberErrorText;
bool _phoneNumberError = false;
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
focusNode: _usernameFocusNode,
decoration: InputDecoration(
labelText: 'Username',
/*
* Step 2
* */
errorText: _userNameErrorText, // Handling error manually
),
textInputAction: TextInputAction.next,
/*
* Step 3
* */
validator: (value) {
setState(() {
if(value.isEmpty) {
_userNameError = true;
_userNameErrorText = 'Enter Username';
}
});
return null; // Return null to handle error manually.
},
/*
* Step 4
* */
onChanged: (value) {
setState(() {
_userNameError = false;
_userNameErrorText = null; // Resets the error
});
},
/*
* Step 5
* */
onFieldSubmitted: (value) {
_formKey.currentState.validate(); // Trigger validation
if(!_userNameError) {
FocusScope.of(context).requestFocus(_phoneNumberFocusNode);
}
},
),
TextFormField(
focusNode: _phoneNumberFocusNode,
decoration: InputDecoration(
labelText: 'Phone Number',
/*
* Step 2
* */
errorText: _phoneNumberErrorText, // Handling error manually
),
textInputAction: TextInputAction.done,
/*
* Step 3
* */
validator: (value) {
setState(() {
if(value.isEmpty) {
_phoneNumberError = true;
_phoneNumberErrorText = 'Enter Phone number';
} else if( value.length < 10) {
_phoneNumberError = true;
_phoneNumberErrorText = 'Invalid Phone number';
}
});
return null; // Return null to handle error manually.
},
/*
* Step 4
* */
onChanged: (value) {
setState(() {
_phoneNumberError = false;
_phoneNumberErrorText = null; // Resets the error
});
},
/*
* Step 5
* */
onFieldSubmitted: (value) {
_formKey.currentState.validate(); // Trigger validation
if(!_phoneNumberError) {
// submit form or whatever your code flow is...
}
},
),
],
),
),
);
}
}
I have achieved your both below functionality:
1) Hide error message when editing
2) validate input field when login button pressed
Note: i have commented phone number regex and put validation for
string length < 10 digit for testing.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState(){
super.initState();
}
final TextEditingController controller = TextEditingController();
// final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10, 15}\$");
bool isError = false;
bool isWriting = false;
bool isLoginPressed = false;
int counter = 0;
String myErrorString = "";
TextSelection currentPosition;
final _textKey = GlobalKey<FormState>();
#override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('MapSample'),
),
body: Container(
child: Column(
children: <Widget>[
Form(
key: _textKey,
child: TextFormField(
controller: controller,
validator: (str) {
myErrorString = "";
if(isLoginPressed){
isError = true;
if (str.isEmpty) {
myErrorString = 'err_empty_field';
return myErrorString;
}
else if (str.length < 10) {
myErrorString = 'err_invalid_phone';
validateMe();
return myErrorString;
}
/*else if (!_phoneRegex.hasMatch(str)) {
myErrorString = 'err_invalid_phone';
validateMe();
return myErrorString;
}*/
isError = false;
myErrorString = "";
}else{
myErrorString = "";
}
},
),
onChanged: () {
counter++;
if(counter == 9){
counter = 0;
isLoginPressed = false;
}
if(isLoginPressed){
}else{
isWriting = true;
isLoginPressed = false;
myErrorString = "";
_textKey.currentState.validate();
}
},
),
RaisedButton(
onPressed: () {
counter = 1;
isWriting = false;
isLoginPressed = true;
_textKey.currentState.validate();
},
child: Text('login'),
)
],
),
),
);
}
void validateMe() {
if(isLoginPressed){
currentPosition = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
String currentText = controller.text;
_textKey.currentState.reset();
controller.text = currentText;
controller.selection = currentPosition;
isWriting = false;
isLoginPressed = true;
}
}
}
I've found working and easier way
final _textKey = GlobalKey<FormState>();
final TextEditingController _controller = TextEditingController();
Widget _getPhoneInputForm() {
final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10,17}");
bool isError = false;
bool isButtonPressed = false;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 36.0),
child: Form(
key: _textKey,
child: TextFormField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: hint_enter_phone,
contentPadding: EdgeInsets.all(24.0),
fillColor: Colors.blueGrey.withOpacity(0.3),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16.0)),
borderSide: BorderSide(color: Colors.blueGrey))),
controller: _controller,
validator: (str) {
if (!isButtonPressed) {
return null;
}
isError = true;
if (str.isEmpty) {
return err_empty_field;
} else if (!_phoneRegex.hasMatch(str)) {
return err_invalid_phone;
}
isError = false;
},
onFieldSubmitted: (str) {
if (_textKey.currentState.validate()) _phoneLogin();
},
),
onChanged: () {
isButtonPressed = false;
if (isError) {
_textKey.currentState.validate();
}
},
),
),
RaisedButton(
color: Colors.teal,
textColor: Colors.white,
onPressed: () {
isButtonPressed = true;
if (_textKey.currentState.validate()) _phoneLogin();
},
child: Text(login),
)
],
);
}
This is an exemple , i think its not necessary to do onchange() , the function validate name do the work ...
String validateName(String value) {
String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(patttern);
if (value.length == 0) {
return "Name is Required";
} else if (!regExp.hasMatch(value)) {
return "Name must be a-z and A-Z";
}
return null;
}
TextFormField(
controller: _lastname, validator: validateName ,
//initialValue: widget.contact.last_name,
decoration:
InputDecoration(labelText: 'Last name'),
),
void Save() {
if (_keyForm.currentState.validate()) {
// No any error in validation
_keyForm.currentState.save();
................
}
i have found that using a combination of FocusNode and AtuoValidateMode.onUserInteraction does the trick.
class _TextAutoValidateModeExampleState extends State<TextAutoValidateModeExample> {
FocusNode node = FocusNode();
#override
Widget build(BuildContext context) {
return Container(
child: TextFormField(
focusNode: node,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if(node.hasFocus) return null;
if (value!.isEmpty) return "value cannot be empty";
if (!value.isEmail) return "not a valid email";
},
),
);
}
}
// Call this method inside onChanged() and when its focusnode hasFocus
void formReset(GlobalKey<FormState> formKey, TextEditingController controller) {
String stringValue = controller.text;
TextPosition textPosition = controller.selection.base;
formKey.currentState.reset();
controller.text = stringValue;
controller.selection = TextSelection.fromPosition(textPosition);
}
this format worked for me, Hope it helps someone....
validator: (value){
bool emailValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value);
isError = true;
if(value.isEmpty){
return "Provide an email";
}else if(!emailValid){
return "Enter a valid email";
}
isError = false;
return null;
},