I created my own TextField widget and i want to use onChange() function of textField, so i created a callbackFunction as you can see below, im trying to print given param of function but receiving null, how can i handle these callbacks;
CallBack Function
void childCallBack(dynamic value, dynamic property) {
setState(() {
property = value;
});
}
How i call my TextField in main state
TextFieldForProduct(
property: widget.product.name,
func: childCallBack,
)
And my CustomTextFieldWidget
class TextFieldForProduct extends StatelessWidget {
TextFieldForProduct({#required this.property, #required this.func});
var property;
Function func;
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) {
func(value, property);
},
),
);
}
}
What is the point of passing widget.product.name to TextFieldForProduct, you can directly set the widget.product.name = value; in the childCallBack since both are available in the same class. Please see the code below:
void childCallBack(String value) {
setState(() {
widget.product.name = value;
});
}
How to call TextField in main state :
TextFieldForProduct(
func: childCallBack,
),
CustomTextFieldWidget
class TextFieldForProduct extends StatelessWidget {
const TextFieldForProduct({#required this.func});
final Function func;
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (String value) => func(value),
),
),
);
}
}
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
What I want to do is disable the elevated button until the text form field is valid. And then once the data is valid the elevated button should be enabled. I have reviewed several SO threads and a few articles on Google about how to disable a button until the text form field is validated. They all focused on whether or not the text form field was empty or not which is not what I am asking about here. I'm using regex to determine if the user has entered a valid email address. Only when the data entered is a valid email is the data considered valid. That is when I want the button to become enabled. If I try to call setState with a boolean in the validateEmail method I get the error:
setState() or markNeedsBuild() called during build.
Any help will be appreciated. Thank you.
class ResetPasswordForm extends StatefulWidget {
const ResetPasswordForm({Key? key}) : super(key: key);
#override
_ResetPasswordFormState createState() => _ResetPasswordFormState();
}
class _ResetPasswordFormState extends State<ResetPasswordForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
String? validateEmail(String? value) {
String pattern = ValidatorRegex.emailAddress;
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty || !regex.hasMatch(value)) {
return ValidatorString.enterValidEmail;
} else {
return null;
}
}
#override
void dispose() {
_emailController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
validator: (value) => validateEmail(value),
),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
Auth().resetPassword(
context,
_emailController.text.trim(),
);
}
},
child: const Text('Reset Password'),
),
],
);
}
}
you can do somthing like that:
class ResetPasswordForm extends StatefulWidget {
const ResetPasswordForm({Key? key}) : super(key: key);
#override
_ResetPasswordFormState createState() => _ResetPasswordFormState();
}
class _ResetPasswordFormState extends State<ResetPasswordForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final bool _isValidated = false;
String? validateEmail(String? value) {
String pattern = ValidatorRegex.emailAddress;
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty || !regex.hasMatch(value)) {
return ValidatorString.enterValidEmail;
} else {
setState(){
_isValidated = true;
}
return null;
}
}
#override
void dispose() {
_emailController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
validator: (value) => validateEmail(value),
),
),
ElevatedButton(
onPressed:_isValidated
? () {
//do stuff
}
: null,,
child: const Text('Reset Password'),
),
],
);
}
}
if onPressed be null, the button is disabled.
Enabling and disabling functionality is the same for most widgets
set onPressed property as shown below
onPressed : null returns a disabled widget while
onPressed: (){} or onPressed: _functionName returns enabled widget
in this case it'll be this way:
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
Auth().resetPassword(
context,
_emailController.text.trim(),
);
} else {
print('disabled');
}
},
child: const Text('Reset Password'),
),
First, move the logic into a named function
void _sendData (){
if (_formKey.currentState!.validate()) {
Auth().resetPassword( context,
_emailController.text.trim(), );
}
Now in onpressed
onpressed: _emailController.text.trim.isNotEmpty?_sendData : null;
Best for this is to just create a form key for this
final formGlobalKey = GlobalKey <FormState> ();
Assign it to form like:
Form(
key: formGlobalKey,
And now you just have to check the validation for this like:
ElevatedButton(
style: style,
onPressed: formGlobalKey.currentState==null?null: formGlobalKey.currentState!.validate()? () {
This is body of button} : null,
child: const Text('Enabled'),
),
**If you didn't use first condition (formGlobalKey.currentState==null?) it will take you towards null exception **
I have a list of objects that I can display in a ListView. Now I wanted to implement a search feature and only display the search result. When I try to do it using onChanged on TextField(or even Controller) it doesn't work. I tried to debug and he gets the list updated correctly but he doesn't update the Widget. But when I removed the onChanged and added a button and then called the same method that I was calling on onChanged everything worked.
The goal is to update the widget as the user writes in the text field.
I would be happy to get some help
My full code :
import 'package:flutter/material.dart';
import 'package:hello_fridge/single_ingredient_icon.dart';
import 'package:string_similarity/string_similarity.dart';
import 'entities/ingredient.dart';
class IngredientsContainer extends StatefulWidget {
const IngredientsContainer({Key? key}) : super(key: key);
#override
_IngredientsContainerState createState() => _IngredientsContainerState();
}
class _IngredientsContainerState extends State<IngredientsContainer> {
late List<Ingredient> ingredients;
final searchController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
searchController.dispose();
super.dispose();
}
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
setState(() {});
}
Widget _searchBar(List<Ingredient> ingredients) {
return Row(
children: <Widget>[
IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.restaurant),
onPressed: null,
),
Expanded(
child: TextField(
controller: searchController,
onChanged: (newValue) {
updateResults(newValue);
},
cursorColor: Colors.black,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
hintText: "Search..."),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(
Icons.search,
color: Color(0xff9ccc65),
),
onPressed: () {
updateResults(searchController.text);
},
),
),
],
);
}
#override
void initState() {
this.ingredients = Ingredient.getDummyIngredients();
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(children: [
Expanded(flex: 1, child: _searchBar(this.ingredients)),
Expanded(flex: 4, child: IngredientsGrid(this.ingredients))
]),
);
}
}
class IngredientsGrid extends StatelessWidget {
List<Ingredient> ingredients;
IngredientsGrid(this.ingredients);
List<Widget> _buildIngredients() {
return this.ingredients.map((ing) => SingleIngredientIcon(ing)).toList();
}
// const IngredientsGrid({
// Key? key,
// }) : super(key: key);
#override
Widget build(BuildContext context) {
this.ingredients.forEach((ing) => print(ing.name! + ","));
return ListView(
children: <Widget>[
GridView.count(
crossAxisCount: 4,
// physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
shrinkWrap: true,
// You won't see infinite size error
children: _buildIngredients()),
// ...... other list children.
],
);
}
}
Moreover, I keep getting this Warning :
"Changing the content within the composing region may cause the input method to behave strangely, and is therefore discouraged. See https://github.com/flutter/flutter/issues/78827 for more details".
Visiting the linked GitHub page wasn't helpful
The problem is that while you are correctly filtering the list but your TextController is not getting assigned any value.
So, no value is getting assigned to your TextField as the initial value and hence the list again filters to have the entire list.
To solve this just assign the TextController the newValue like this.
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
// change
searchController = TextEditingController.fromValue(
TextEditingValue(
text: newValue,
),
);
setState(() {});
}
If it throws an error then remove final from the variable declaration, like this :
var searchController = TextEditingController();
import 'package:flutter/material.dart';
class Demo {
int no;
String value;
Demo({this.value, this.no});
}
class Control {
TextEditingController controller;
FocusNode node;
Control({this.controller, this.node});
}
class DemoPage extends StatefulWidget {
static const routeName = '/Demo';
DemoPage({Key key}) : super(key: key);
#override
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
List<Demo> txtfield;
List<Control> control;
#override
void initState() {
txtfield = [];
control = [];
// no = 0;
add();
super.initState();
}
int no;
void add() {
no = (no ?? 0) + 1;
setState(() {});
txtfield.add(Demo(no: no));
control.add(Control(
controller: TextEditingController(),
node: FocusNode(),
));
// no = no +1;
}
#override
Widget build(BuildContext context) {
// print(txtfield[0].no);
// FocusScope.of(context).requestFocus(control[control.length - 1].node);
return Scaffold(
appBar: AppBar(),
body: Center(
child: Card(
child: Container(
child: Column(
children: txtfield
.map((f) => TextField(
controller: control[f.no - 1].controller,
focusNode: control[f.no - 1].node,
autofocus: true,
))
.toList(),
),
width: 400,
padding: EdgeInsets.all(20),
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
add();
print(no);
FocusScope.of(context).requestFocus(control[control.length - 1].node);
},
),
);
}
}
I used above code. but I can't able to focus on a newly added text field.
when I check for that newly added text field has focus, it shows true, but I can't able to write anything in that text field.
I don't know what is an error in that code.
I search for this solution for more than 4 days. but I can't able to find solution.
At the onPressed of your floatingActionButton change this line:
FocusScope.of(context).requestFocus(control[control.length - 1].node);
with this
control[control.length - 1].node.requestFocus();
I've implemented a date selection in my flutter app. The selected date then gets displayed, but as soon as the focus changes, the selected DateTime object becomes null without any reason.
To store the DateTime and some other variables I've created an object which is stored as a final variable in the StatefulWidget class. This object also includes Strings which don't change when the DateTime becomes null.
import 'package:flutter/material.dart';
import 'package:smartyne/core/assignment.dart';
class AssignmentPage extends StatefulWidget {
final bool editMode;
final Assignment assignment;
AssignmentPage(this.assignment, this.editMode);
#override
_AssignmentPageState createState() => _AssignmentPageState();
}
class _AssignmentPageState extends State<AssignmentPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(), labelText: "Title"),
onChanged: (value) {
widget.assignment.title = value;
setState(() {});
},
),
...
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
MaterialButton(
child: Text(widget.assignment.deadlineAsString),
onPressed: () {
_showDatePicker();
},
),
...
],
)
],
),
));
}
void _showDatePicker() async {
final DateTime newDeadline = await showDatePicker(
context: context,
initialDate: widget.assignment.deadline == null ? DateTime.now() : widget.assignment.deadline,
firstDate: DateTime(1900),
lastDate: DateTime(2099)
);
if (newDeadline != null && newDeadline != widget.assignment.deadline) {
setState(() {
widget.assignment.deadline = newDeadline;
});
}
}
}
As shown above, the DateTime gets selected by a DatePicker and the result is then stored in the assignment object. But when you click on the TextField for the title, it suddenly turns null.
Example gif: https://gph.is/g/aKn7yqq
Use the declared variable inside state class like below
import 'package:flutter/material.dart';
import 'package:smartyne/core/assignment.dart';
class AssignmentPage extends StatefulWidget {
#override
_AssignmentPageState createState() => _AssignmentPageState();
}
class _AssignmentPageState extends State<AssignmentPage> {
final bool editMode;
final Assignment assignment;
#override
Widget build(BuildContext context) {
return new Container();
//access it like
//Text(assignment.deadlineAsString),
//instead of
//Text(widget.assignment.deadlineAsString),
}