TextField widgets lose focus (Flutter) - flutter

I am creating an app with Flutter TextField widgets:
class CategoryData {
int? id;
String name;
String description;
CategoryData({this.id, required this.name, required this.description});
}
class CategoriesEdit extends StatefulWidget {
Database? db;
CategoryData? category;
CategoriesEdit({super.key, required this.db, required this.category});
#override
State<StatefulWidget> createState() => CategoriesEditState();
}
class CategoriesEditState extends State<CategoriesEdit> {
CategoryData? category;
void saveState(BuildContext context) {
// ...
}
#override
Widget build(BuildContext context) {
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: const Icon(Icons.arrow_circle_left),
onTap: () => Navigator.pop(context)),
title: const Text("Edit Category"),
),
body: Column(children: [
Column(key: const Key('name'), children: [
const Text("Category name:*"),
TextField(
controller: TextEditingController(text: category!.name),
onChanged: (value) {
setState(() {
category!.name = value;
});
})
]),
Column(key: const Key('description'), children: [
const Text("Description:"),
TextField(
controller: TextEditingController(text: category!.description),
onChanged: (value) {
setState(() {
category!.description = value;
});
})
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
ElevatedButton(
onPressed: () => saveState(context), // passing false
child: const Text('OK'),
),
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
// passing false
child: const Text('Cancel'),
),
]),
]));
}
}
But after I type a character in one of these two widgets, the cursor moves before the first character and the Android keyboard widget disappears. Why? And how to fix that bug?
I tried adding widget keys, but as you see it didn't help.

There is a lot of things going wrong here, not only the stuff mentioned in the other answer.
Move the setState in the builder into initState:
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
Don't use setState in the onChanged callback. Change:
onChanged: (value) {
setState(() {
category!.description = value;
});
}
to this:
onChanged: (value) {
category!.description = value;
}
Store the TextEditingControllers, because you have to dispose them once we dispose the state.
If you are already using TextEditingControllers, then you don't need the onChanged callback. Just take text from the controller like explained in the other answer.

You do not have to do
controller: TextEditingController(text: category!.name)
because the controller's text automatically changes once you connect it to TextField.
The reason is once you set some text to the controller, it re-applies the text thus moving the cursor to the front.
I have solved this for you :
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CategoryData {
int? id;
String name;
String description;
CategoryData({this.id, required this.name, required this.description});
}
class CategoriesEdit extends StatefulWidget {
CategoryData? category;
CategoriesEdit({required this.category});
#override
State<StatefulWidget> createState() => CategoriesEditState();
}
class CategoriesEditState extends State<CategoriesEdit> {
CategoryData? category;
// Database? db;
TextEditingController nametextController = TextEditingController();
TextEditingController descriptionTextController = TextEditingController();
void saveState(BuildContext context) {
// ...
}
#override
Widget build(BuildContext context) {
if (category == null) {
setState(() {
category = widget.category ?? CategoryData(name: "", description: "");
});
}
nametextController.text = category!.name??"";
descriptionTextController.text = category!.description??"";
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: const Icon(Icons.arrow_circle_left),
onTap: () => Navigator.pop(context)),
title: const Text("Edit Category"),
),
body: Column(children: [
Column(key: const Key('name'), children: [
const Text("Category name:*"),
TextField(
controller: nametextController,
onChanged: (value) {
setState(() {
category!.name = value;
});
})
]),
Column(key: const Key('description'), children: [
const Text("Description:"),
TextField(
controller: descriptionTextController,
onChanged: (value) {
setState(() {
category!.description = value;
});
})
]),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
ElevatedButton(
onPressed: () => saveState(context), // passing false
child: const Text('OK'),
),
OutlinedButton(
onPressed: () => Navigator.pop(context, false),
// passing false
child: const Text('Cancel'),
),
]),
]));
}
}
I have tested this code and it is working fine, let me know if you have any doubt. Hope this helps you.

Related

in dynamic form list removing index not working

I've recently asked a question on how to create a group of form dynamically. and i've got an answer. but the problem was when removed an index of the group it removes the last added form. but the value is correct.
for example if i add 3 dynamic group formfields and removed the second index index[1] the ui update will remove the last index but the removed value is only the selected index. why is the ui not working as expected?
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
Text('phone'),
Text(list[index].phone),
Text('email'),
Text(list[index].email),
Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return MyForm(
// dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
list[index].setPhone(phoneVal);
setState(() {});
}
},
// every changes here will update your current list value
onchangeEmail: (emailVal) {
if (emailVal != null) {
list[index].setEmail(emailVal);
setState(() {});
}
},
onchangeCategory: (categoryVal) {
if (categoryVal != null) {
list[index].setCategory(categoryVal);
setState(() {});
}
},
onremove: () {
list.removeAt(index);
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final Function(String?) onchangeEmail;
final Function(String?) onchangeCategory;
final VoidCallback? onremove;
const MyForm({
key,
required this.initInfo,
required this.onChangePhone,
required this.onchangeEmail,
required this.onchangeCategory,
required this.onremove,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
TextEditingController _phoneCtrl = TextEditingController();
TextEditingController _emailCtrl = TextEditingController();
String? selected;
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
void initState() {
super.initState();
// set init value
_phoneCtrl = TextEditingController(text: widget.initInfo.phone);
_emailCtrl = TextEditingController(text: widget.initInfo.email);
selected = widget.initInfo.category;
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
TextFormField(
controller: _phoneCtrl,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: _emailCtrl,
onChanged: widget.onchangeEmail,
),
DropdownButtonFormField2(
//key: _key,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
buttonHeight: 60,
buttonPadding: const EdgeInsets.only(left: 20, right: 10),
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
validator: (value) {
if (value == null) {
return 'Please select Catagory.';
}
},
onChanged: widget.onchangeCategory,
onSaved: widget.onchangeCategory)
/// same like TextFormField, you can create new widget below
/// for dropdown, you have to 2 required value
/// the initValue and the onchage function
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _email = '';
String _category = '';
/// getter
String get phone => _phone;
String get email => _email;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setEmail(String email) {
_email = email;
}
void setCategory(String category) {
_category = category;
}
}
any help is appreciated.

Access data from custom widget created on different class in flutter

I new to Flutter and i was trying to find a solution for the below issue for several hours. I have searched and every solution provided does not work form me.
I have page where one of the widgets is the autocomplete text input. I have created this autocomplete widget on different class. I have added this widget as StatefulBuilder within my main widget. it is working fine however, i am not able to access its value so I can store it with other fields.
My code look like
class ItemDetails extends StatefulWidget {
const ItemDetails({Key? key}) : super(key: key);
static const routeName = '/item_details';
#override
State<ItemDetails> createState() => _ItemDetails();
}
class _ItemDetails extends State<ItemDetails> {
late TextEditingController labelController;
late TextEditingController valueController;
late TextEditingController notesController;
bool _submitted = false;
late var args;
String _itemLabel2 = "";
// var labelAutoComp = LabelSugg();
#override
void initState() {
super.initState();
labelController = TextEditingController();
valueController = TextEditingController();
notesController = TextEditingController();
}
#override
void dispose() {
labelController.dispose();
valueController.dispose();
notesController.dispose();
// Hive.close();
super.dispose();
}
String? _labelErrorText(context) {
final text = labelController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
String? _valueErrorText(context) {
final text = valueController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
#override
Widget build(BuildContext context) {
try {
args = ModalRoute.of(context)!.settings.arguments as Map;
} on Exception catch (e) {
// print(e);
}
// print(args);
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LabelSugg(getLabelText: (String val) {
print(val);
_itemLabel2 = val;
}),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.label,
hintText: AppLocalizations.of(context)!.labelHint,
errorText:
_submitted ? _labelErrorText(context) : null,
),
controller: labelController,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.value,
hintText: AppLocalizations.of(context)!.valueHint,
errorText:
_submitted ? _valueErrorText(context) : null,
),
controller: valueController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: false),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
TextInputFormatter.withFunction(
(oldValue, newValue) {
try {
final text = newValue.text;
if (text.isNotEmpty) double.parse(text);
return newValue;
} catch (e) {}
return oldValue;
}),
], // Only numbers can be entered
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.notes,
hintText: AppLocalizations.of(context)!.noteHint,
),
controller: notesController,
onChanged: (_) => setState(() {}),
),
]),
// ],
),
Expanded(
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() => _submitted = true);
if (_labelErrorText(context) == null &&
_valueErrorText(context) == null) {
//insert
var localLabel = labelController.value.text;
var _localValue = 0.0;
if (valueController.value.text != '') {
_localValue =
double.parse(valueController.value.text);
} else {
_localValue = 0.0;
}
var localNotes = notesController.value.text;
addItemToList(
localLabel, _localValue, localNotes);
Navigator.of(context).pop();
labelController.clear();
valueController.clear();
notesController.clear();
}
},
label: Text(AppLocalizations.of(context)!.add),
icon: const Icon(Icons.save, size: 18),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => {Navigator.pop(context)},
label: Text(AppLocalizations.of(context)!.cancel),
icon: const Icon(Icons.cancel, size: 18),
),
],
)),
),
// )
],
)));
}
void addItemToList(String localLabel, double localValue, String localNotes) {
var _item = YearItems()..yearID = args['year'];
_item.itemLabel = localLabel;
_item.itemValue = localValue;
_item.itemNote = localNotes;
print(_itemLabel2);
final itemsBox = ItemsBoxes.getTransactions();
itemsBox.add(_item);
}
}
my labelAutoComp widget code look like
class LabelSugg extends StatefulWidget {
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController2;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
getLabel() {
return widget.getLabelText(fieldTextEditingController2.text);
}
#override
Widget build(BuildContext context) {
List<LabelsAc> labelOptions = <LabelsAc>[
LabelsAc(label: AppLocalizations.of(context)!.labelClothes),
LabelsAc(label: AppLocalizations.of(context)!.labelFood),
LabelsAc(label: AppLocalizations.of(context)!.labelPerfumes),
LabelsAc(label: AppLocalizations.of(context)!.labelCapital),
];
return Autocomplete<LabelsAc>(
optionsBuilder: (TextEditingValue textEditingValue) {
return labelOptions
.where((LabelsAc _label) => _label.label
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase()))
.toList();
},
displayStringForOption: (LabelsAc option) => option.label,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
// fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: const TextStyle(fontWeight: FontWeight.bold),
// onChanged: getLabel(),
onChanged: (String val) {
fieldTextEditingController2 = fieldTextEditingController;
getLabel();
});
},
onSelected: (LabelsAc selection) {
fieldTextEditingController2 =
TextEditingController(text: selection.label);
getLabel();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<LabelsAc> onSelected,
Iterable<LabelsAc> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: Container(
// width: 350,
// color: Colors.cyan,
child: ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final LabelsAc option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.label,
style: const TextStyle(color: Colors.black)),
),
);
},
),
),
),
);
},
);
// ),
// );
}
}
class LabelsAc {
LabelsAc({required this.label});
String label;
}
first is redundant when you wrap your class that extend StatefullWidget with StatefullBuilder. LabelSugg is a component Widget. you can use it like other widget.
benefit to separate widget with StatefullWidget class is, we can update the value inside the class without re-build the current page. which is good for performance. that's why developer recomend to separete with class insted compared to make local method.
as you see, when you create LabelSugg extend StatefullWidget class , we will have _LabelSugg . underscore means that: all variable only accessible on current file.
thats why we can't call getLabel() or other variable from different file.
its used for handle the State in 'LabelSugg` widget.
now how to pass the value from LabelSugg is by created variable outside the state. here you are:
class LabelSugg extends StatefulWidget {
// use this to pass any changes when we use LabelSugg
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
then we can call the onChaged inside _LabelSugg state. because its Statefull widget, we can acces by : widget.getLabelText()
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController;
.....
getLabel() {
return widget.getLabelText(fieldTextEditingController.text);
}
then in other class we call LabelSugg like common widget
import 'package:../labelsug.dart';
class ItemDetails extends StatefulWidget {
.....
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
// now use it like a widget
LabelSug(
getLabelText: (String val){
print(val);
}
:)

CheckboxListTile state not changing when clicked inspite of calling setState

I am creating a List of checkboxes using ListView and CheckboxListTile as below. However, when I click or unclick on an item, the CheckboxListTile does not update and reflect the check or uncheck.
The code creating the ListView is below:
void _showDialog(context) {
showDialog(
context: context,
builder: (context) {
return Material(
child: ListView(
children: this.selectionList.map((e) {
return CheckboxListTile(
value: e.isSelected,
title: Text(e.name),
onChanged: (value) {
setState(() {
e.isSelected = value ?? false;
});
});
}).toList()),
);
});
}
Also including the full code:
import 'package:flutter/material.dart';
import 'package:recipevault/models/user_profile.dart';
class MultiSelectComponent extends StatefulWidget {
List<Interest> displayList; //initial values are marked as selected=true
MultiSelectComponent({Key? key, this.displayList = const <Interest>[]})
: super(key: key);
#override
_MultiSelectComponentState createState() => _MultiSelectComponentState();
}
class _MultiSelectComponentState extends State<MultiSelectComponent> {
List<Interest> selectionList = <Interest>[];
#override
void initState() {
// TODO: implement initState
super.initState();
setState(() {
this.selectionList = List.from(widget.displayList);
});
}
void _showDialog(context) {
showDialog(
context: context,
builder: (context) {
return Material(
child: ListView(
children: this.selectionList.map((e) {
return CheckboxListTile(
value: e.isSelected,
title: Text(e.name),
onChanged: (value) {
setState(() {
e.isSelected = value ?? false;
});
});
}).toList()),
);
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () {
_showDialog(context);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 1.2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Select"),
Icon(Icons.arrow_downward),
],
),
),
),
Wrap(children: [
...selectionList
.where((element) => element.isSelected)
.map((e) => Chip(
label: Text(e.name),
onDeleted: () {
setState(() {
e.isSelected = false;
});
},
))
.toList(),
]),
]);
}
}
class Interest {
final String type, name;
final int id;
bool isSelected;
Interest(
{required this.id,
required this.name,
required this.type,
this.isSelected = false});
Interest.fromJson(Map<String, Object?> json)
: this(
id: json['id']! as int,
type: json['type']! as String,
name: json['name']! as String,
);
Map<String, Object?> toJson() {
return {
'id': this.id,
'type': this.type,
'name': this.name,
};
}
You have to extract the content of showDialog's builder to a separate StatefulWidget.

RadioListTile is not working with setstate

What is working when i click on radio button it is not changing
class OnBoardingCheckBoxWithActionWidget extends StatefulWidget {
String label;
Function onClick;
OnBoardingCheckBoxWithActionWidget(this.label, {this.onClick});
#override
_OnBoardingCheckBoxWithActionWidgetState createState() =>
_OnBoardingCheckBoxWithActionWidgetState();
}
class _OnBoardingCheckBoxWithActionWidgetState
extends State<OnBoardingCheckBoxWithActionWidget> {
bool checked = true;
#override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: RadioListTile(
value: checked,
groupValue: checked,
toggleable: true,
title: Text("${widget.label}"),
onChanged: (val) {
if (widget.onClick != null) {
widget.onClick(val);
}
setState(() {
print("$checked");
checked = !checked;
print("$checked");
});
},
),
),
); // Card(child: Row(children: [Expanded(child: RadioListTile())],),);
}
}
You're trying to change some value from your Parent Stateful widget.
Try changing your current widget like
class OnBoardingCheckBoxWithActionWidget
extends StatelessWidget {
String label;
Function onClick;
var groupValue;
var buttonValue;
OnBoardingCheckBoxWithActionWidget(this.label, {this.onClick, this.groupValue,, this.buttonValue});
#override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: RadioListTile(
value: buttonValue,
groupValue: groupValue,
toggleable: true,
title: Text("${this.label}"),
onChanged: (val) {
if (this.onClick != null) {
this.onClick(val);
}
},
),
),
); // Card(child: Row(children: [Expanded(child: RadioListTile())],),);
}
}
And then call it like
OnBoardingCheckBoxWithActionWidget("Button1", onClick: (value){
setState((){
///Your Other Code
if(value){
groupValue = 'button1';
}
});
},
groupValue: someGroupValueVariableInState, buttonValue: 'button1')
Note that, the groupValue and buttonValue variables will always be some other variables other than booleans. When groupValue == buttonValue, the corresponding button will automatically marked as checked.

Flutter make a form with provider and riverpod

I'm new with Flutter and I want to upgrade my code. I have a form that uses multiple textformfields and I want to convert this code using provider and riverpod to improve readability but I'm not sure how to do it.
For the example I simplified my code to only one distance field but there are many others.
This is my CalculatorScreen :
import 'dart:async' show Future;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:app/core/models/model_form_calculator.dart';
import 'package:app/core/services/service_form_validator.dart';
import 'package:app/core/utils/utils_app_color.dart';
class CalculatorScreen extends StatefulWidget
{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen>
{
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _formKey = GlobalKey<FormState>();
FormCalculatorModel _formData = FormCalculatorModel();
bool _autoValidateForm = false;
final TextEditingController _controllerDistance = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose()
{
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context)
{
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
key: _scaffoldKey,
backgroundColor: AppColors.colorBgDark,
body : _buildBody()
),
);
}
Widget _buildBody()
{
return SingleChildScrollView(
child: Column(
children: [
Form(
key: _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _controllerDistance,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "Enter a value",
),
validator: (value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formData.distance = num.tryParse(value).round();
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: FlatButton(
child: Text("Erase"),
onPressed: _buttonResetAction
),
),
Expanded(
child: FlatButton(
child: Text("Send"),
onPressed: _buttonSubmitAction
),
),
],
),
]
),
),
],
),
);
}
void _buttonResetAction()
{
_eraseForm();
}
void _eraseForm(){
setState(() {
_formKey.currentState.reset();
_formData = FormCalculatorModel();
_autoValidateForm = false;
_controllerDistance.clear();
});
}
void _buttonSubmitAction() async
{
if (!_formKey.currentState.validate()) {
setState(() {
_autoValidateForm = true;
});
return;
}
_formKey.currentState.save();
try{
// some actions
}catch(e){
_eraseForm();
print(e.toString());
}
}
}
This is my formModel (This model contains all the fields that I can fill in my form and allows me to store the values ​​of the form once validated to then make calculations with these values
):
class FormCalculatorModel{
int distance;
FormCalculatorModel({
this.distance,
});
#override
String toString() {
return '{ '
'${this.distance}, '
'}';
}
}
And my FormValidatorService :
class FormValidatorService{
static String isDistanceValid(String value)
{
num _distance = num.tryParse(value);
if (_distance == null) {
return "is required";
}
if (_distance < 200) {
return "Min distance is 200";
}
if (_distance > 1000) {
return "Max dist is 1000";
}
return null;
}
}
Now I want to convert this with riverpod. I'm a little lost, there are few examples on the internet and I don't really see how to manage my form
At first I'm just trying to handle the validation of the form but it doesn't work.
My calculatorScreen :
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class CalculatorScreen extends HookWidget{
final _formKey = GlobalKey<FormState>();
bool _autoValidateForm = false;
FormCalculatorModel _formData = FormCalculatorModel();
final TextEditingController _controllerDistance = TextEditingController();
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context){
final _formModel = useProvider(formCalculatorProvider.state);
return SingleChildScrollView(
child: Column(
children: [
TitleComponent(
title: "Calcul",
description: "Parametrer",
),
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
//errorText: _formModel.distance.error,
),
controller: _controllerDistance,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {_formData.distance = num.tryParse(value).round();}
),
],
),
),
ButtonComponent.primary(
text: "Calculer",
context: context,
onPressed : context.read(formCalculatorProvider).submitData(key: _formKey),
),
],
)
],
),
);
}
}
And my FormCalculatorNotifier :
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
enum FormState
{
EMPTY,
SUCCESS,
ERROR
}
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.autoValidate, this.distance});
final FormState formState;
final bool autoValidate;
final String distance;
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew>
{
FormCalculatorNotifier() : super(_initial);
static const FormState _initialState = FormState.EMPTY;
static const _initial = FormCalculatorModelNew(
formState : _initialState,
autoValidate: false,
distance: null
);
submitData({key}){
print(key);
if (!key.currentState.validate()) {
state = FormCalculatorModelNew(
autoValidate: true,
);
return;
}
key.currentState.save();
}
}
The provider :
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
It does not really make sense to use Provider in your example code because I don't see anywhere listen to the state of formCalculatorProvider. Also, the form itself should be managed in the form widget itself.
I assume you want to share the distance value with other widgets. Here are what I will do:
_autoValidate: leave it inside the widget and handle it by Hook
add copyWith inside FormCalculatorModelNew (can easily update partial value)
formCalculatorProvider part:
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
enum MyFormState { EMPTY, SUCCESS, ERROR }
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.distance});
final MyFormState formState;
final int distance;
FormCalculatorModelNew copyWith({
MyFormState formState,
int distance,
}) {
return FormCalculatorModelNew(
formState: formState ?? this.formState,
distance: distance ?? this.distance,
);
}
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew> {
FormCalculatorNotifier() : super(_initial);
static const MyFormState _initialState = MyFormState.EMPTY;
static const _initial =
FormCalculatorModelNew(formState: _initialState, distance: null);
void update(int distance) {
state = state.copyWith(distance: distance, formState: MyFormState.SUCCESS);
}
void error() {
state = state.copyWith(distance: null, formState: MyFormState.ERROR);
}
void clear() {
state = state.copyWith(distance: null, formState: MyFormState.EMPTY);
}
}
CalculatorScreen part: (simplify)
class CalculatorScreen extends HookWidget {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
final _autoValidate = useState<bool>(false);
final _controller = useTextEditingController();
return Scaffold(
body: Form(
key: _formKey,
autovalidate: _autoValidate.value,
child: Column(
children: [
TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
validator: (value) {
return FormValidatorService.isDistanceValid(value);
},
onSaved: (value) {
context.read(formCalculatorProvider).update(num.tryParse(value).round());
},
),
Row(
children: [
FlatButton(
child: Text('Erase'),
onPressed: () {
_formKey.currentState.reset();
_controller.clear();
_autoValidate.value = false;
context.read(formCalculatorProvider).clear();
},
),
FlatButton(
child: Text('Send'),
onPressed: () {
if(_formKey.currentState.validate()){
_formKey.currentState.save();
}else{
_autoValidate.value = true;
context.read(formCalculatorProvider).error();
}
},
),
],
),
],
),
),
);
}
}
You can use TextEditingController.
Further create a provider like so, and you may now listen to text changes and store them where desired using the same provider
final formControllerProvider =
StateProvider<TextEditingController>((ref) => TextEditingController());