I am trying to use Riverpod state management. I have two TextFormField and I want to set the value of a Text by taking the sum of the values entered in each of the fields using a StateNotifierProvider.
In the following code, CashCounterData is a data model to be used by the StateNotifier, CashCounter. The notifier has two methods, setCount and setCash that are called in the onChanged method of each TextFormField.
final cashProvider = StateNotifierProvider<CashCounter, CashCounterData>((ref) => CashCounter());
class CashCounter extends StateNotifier<CashCounterData> {
CashCounter() : super(_initialData);
static const _initialData = CashCounterData(0, 0);
void setCount(int value){
state = CashCounterData(value, state.cash);
}
void setCash(value){
state = CashCounterData(state.count, value);
}
int get count => state.count;
int get cash => state.cash;
}
class CashCounterData {
final int count;
final int cash;
const CashCounterData(this.count, this.cash);
}
Next, I implemented the UI and am trying to tie in the StateNotifierProvider defined above. However, when I enter values into each TextFormField, the Text widget is always displaying 0.
class CalculatableTextFormField extends HookWidget {
#override
Widget build(BuildContext context) {
final cashCounterProvider = useProvider(cashProvider.notifier);
final TextEditingController _count = TextEditingController();
final TextEditingController _cash = TextEditingController();
return Scaffold(
body: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${cashCounterProvider.count + cashCounterProvider.cash}'
),
TextFormField(
controller: _count,
keyboardType: TextInputType.number,
onChanged: (value)=>cashCounterProvider.setCount(int.parse(value)),
),
TextFormField(
controller: _cash,
keyboardType: TextInputType.number,
onChanged: (value)=>cashCounterProvider.setCash(int.parse(value)),
)
],
),
),
);
}
}
What am I missing to get my desired behavior?
You are watching the notifier, not the state. The state is what gets changed, and therefore notifies listeners.
It should work if you just change:
final cashCounterProvider = useProvider(cashProvider.notifier);
to:
final cashCounterProvider = useProvider(cashProvider);
Then, in your change handlers:
onChanged: (value) => context.read(cashProvider.notifier).setCash(int.tryParse(value) ?? 0),
When using a provider in a handler like this, prefer context.read as demonstrated above to avoid unnecessary rebuilds.
You also need to use hooks if you are putting your TextEditingControllers in the build method.
final TextEditingController _count = useTextEditingController();
final TextEditingController _cash = useTextEditingController();
All together, your solution is the following:
class CalculatableTextFormField extends HookWidget {
#override
Widget build(BuildContext context) {
final cashCounterProvider = useProvider(cashProvider);
final TextEditingController _count = useTextEditingController();
final TextEditingController _cash = useTextEditingController();
return Scaffold(
body: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${cashCounterProvider.count + cashCounterProvider.cash}'),
TextFormField(
controller: _count,
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read(cashProvider.notifier).setCount(int.tryParse(value) ?? 0),
),
TextFormField(
controller: _cash,
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read(cashProvider.notifier).setCash(int.tryParse(value) ?? 0),
)
],
),
),
);
}
}
Related
class UserInputArea extends StatefulWidget {
#override
State<UserInputArea> createState() => _UserInputAreaState();
}
class _UserInputAreaState extends State<UserInputArea> {
#override
Widget build(BuildContext context) {
String convertedText='';
setState(() {
convertedText = Provider.of<UserText>(context, listen: true).convertedText;
print('convertedText :: $convertedText');
});
return Card(
elevation: 10,
child: Container(
padding: EdgeInsets.all(10),
child: TextField(
decoration: InputDecoration(hintText: convertedText.isNotEmpty ? convertedText : 'Enter text'),
keyboardType: TextInputType.multiline,
maxLines: 5,
onChanged: (value){
Provider.of<UserText>(context, listen: false).updateText(value);
},
),
),
);
}
}
Need to update hintText field whenever convertedText gets updated.
This update is happening only if screen refreshed somehow (In Appbar, if click on home-button-icon the data get updated in TextField), Using Provider package that should listen the changes and update the required feild, didnot work. So converted page to Stateful widget and addedd setState() & moved convertedText variable inside it. But still its not working, and not able to figure it out, what is exactly missing here? Anyhelp appreciated. Thanks in advance
Please use TextEditingController class
your code will be somthing like this
class UserInputArea extends StatefulWidget {
#override
State<UserInputArea> createState() => _UserInputAreaState();
}
class _UserInputAreaState extends State<UserInputArea> {
final TextEditingController nameController = TextEditingController();
#override
void initState() {
nameController.text = "test";
super.initState();
//Here you should write your func to change the controller value
Future.delayed(const Duration(seconds: 2), () {
nameController.text = 'test after chabging';
});
}
#override
Widget build(BuildContext context) {
return Card(
elevation: 10,
child: Container(
padding: EdgeInsets.all(10),
child: TextField(
controller: nameController,
decoration: InputDecoration(hintText: convertedText.isNotEmpty ? convertedText : 'Enter text'),
keyboardType: TextInputType.multiline,
maxLines: 5,
),
),
);
}
}
in the write it code above when you will enter the page the hint text will be test after 2 seconds the value will be "test after chabging" without any problem you do not need setState(() {}) I tired it and it works
I think that putting SetState() into the method and calling the method from onChanged could solve the issue. And moving it from Widget build. Something like this:
class UserInputArea extends StatefulWidget {
#override
State<UserInputArea> createState() => _UserInputAreaState();
}
class _UserInputAreaState extends State<UserInputArea> {
String convertedText='';
void _updateField() {
setState(() {
convertedText = Provider.of<UserText>(context, listen: true).convertedText;
print('convertedText :: $convertedText');
});
#override
Widget build(BuildContext context) {
return Card(
elevation: 10,
child: Container(
padding: EdgeInsets.all(10),
child: TextField(
decoration: InputDecoration(hintText: convertedText.isNotEmpty ? convertedText : 'Enter text'),
keyboardType: TextInputType.multiline,
maxLines: 5,
onChanged: (value){
Provider.of<UserText>(context, listen: false).updateText(value);
_updateField();
},
),
),
);
}
}
I am experimenting with flutter and I have a very simple code as follows:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
static double d = 0;
static TextEditingController editingController = TextEditingController();
#override
void initState() {
editingController.addListener(() {
setState(() {});
});
super.initState();
}
#override
void dispose() {
editingController.dispose();
super.dispose();
}
Calc calc = Calc(d: d, e: editingController);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextField(
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
if (value.isNotEmpty) {
d = double.parse(value);
} else if (value.isEmpty) {
d = 0;
}
});
},
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'(^(\d{1,})\.?(\d{0,2}))'),
),
],
),
TextField(
keyboardType: TextInputType.number,
controller: editingController,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'(^(\d{1,})\.?(\d{0,2}))'),
),
],
),
Text(
'First Text Field Value + 2 = \n${calc.dString()}',
style: TextStyle(fontSize: 30, color: Colors.purpleAccent),
),
Text(
calc.eString(),
style: TextStyle(fontSize: 30, color: Colors.deepOrangeAccent),
),
],
),
),
),
);
}
}
class Calc {
final double d;
final TextEditingController e;
Calc({this.d, this.e});
String dString() {
double result = d + 2;
return result.toStringAsFixed(0);
}
String eString() {
return e.text;
}
}
As we can see I am passing both the text fields' values into another class for some math and getting the results. These results are displayed using the Text widgets.
For the 1st TextField, I have used onChange method, and for the 2nd TextField, I have used TextEditingController.
I get return value for 2nd TextField from the Calc class, but not for the 1st TextField!
I think I am missing something basic and I did not find any solution so far. Can you please help me what am I missing here.
1st of all, you are creating just a single object of Calc,
yes as you can see your 2nd textField update perfectly because it's using TextEditingController but for the 1st one, it just call once and become 2 because of dString(), while on 1st run d becomes 0 passed on Calc.
if you want to use Calc to update text, you can simply put it inside build method like this , i dont suggest it, you can use callBackMethod to handle this or use another TextEditingController.
Hope you get it now
#override
Widget build(BuildContext context) {
Calc calc = Calc(d: d, e: editingController);
return Scaffold(
body: Center(
Your Calc Object is not being affect by setState() call. To run be able to get value of the calcobject, run it in you onChanged() function.
In my last question about StateNotifierProvider not updating state using HookWidget my issue as calculating two TextFormField has been solved, now instead of using HookWidget i try to implement that with ConsumerWidget. i migrated my last code to:
class CalculateTextFormField extends ConsumerWidget {
#override
Widget build(BuildContext context,ScopedReader watch) {
final cashCounterProvider = watch(cashProvider);
final TextEditingController _count = TextEditingController();
final TextEditingController _cash = TextEditingController();
return Scaffold(
body: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${cashCounterProvider.count + cashCounterProvider.cash}'),
TextFormField(
controller: _count,
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read(cashProvider.notifier).setCount(int.tryParse(value) ?? 0),
),
TextFormField(
controller: _cash,
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read(cashProvider.notifier).setCash(int.tryParse(value) ?? 0),
)
],
),
),
);
}
}
final cashProvider = StateNotifierProvider<CashCounter, CashCounterData>((ref) => CashCounter());
class CashCounter extends StateNotifier<CashCounterData> {
CashCounter() : super(_initialData);
static const _initialData = CashCounterData(0, 0);
void setCount(int value){
state = CashCounterData(value, state.cash);
}
void setCash(value){
state = CashCounterData(state.count, value);
}
int get count => state.count;
int get cash => state.cash;
}
class CashCounterData {
final int count;
final int cash;
const CashCounterData(this.count, this.cash);
}
here i defined cashCounterProvider and when i try to enter value into TextFormField i can't enter multiple value and this line as
Text('${cashCounterProvider.count + cashCounterProvider.cash}'),
does work once. i try to implemeting a simple calculator on multiple entering value into inputs
Controllers in Flutter need to be declared outside the build method unless using hooks. Whenever you type into one of your TextFields, the widget is being rebuilt, which causes your controller to be reassigned.
If using hooks, the following is fine:
class CalculatableTextFormField extends HookWidget {
#override
Widget build(BuildContext context) {
final cashCounterProvider = useProvider(cashProvider);
final TextEditingController _count = useTextEditingController();
final TextEditingController _cash = useTextEditingController();
...
Otherwise, declare controllers outside the build method:
class CalculatableTextFormField extends ConsumerWidget {
final TextEditingController _count = TextEditingController();
final TextEditingController _cash = TextEditingController();
#override
Widget build(BuildContext context, ScopedReader watch) {
final cashCounterProvider = watch(cashProvider);
...
One of my favorite benefits of hooks is not having to handle disposal logic on your own. Here is an example of handling the lifecycle of a TextEditingController. After reading that, take a look at the implementation for useTextEditingController. That should help make sense of how things are working.
how can i set the value of a TextFormField from a widget variable when this variable changed according to a setstate action after building the widget and not by the initial value of this textformfeild
what i know that i can change its value by using the TextEditingController but i have too many TextFormField and creating the TextEditingController take a lot of code by using the traditional way by adding them the whole widget:
1-Create a TextEditingController.
2-Connect the TextEditingController to a text field.
3-Create a function to update the latest value.
4-Listen to the controller for changes.
is there any way to build this controller and do those steps inside the code of the TextFormField only? or another way ?
place controler in
TextFormField(
controller: TextEditingController(text: daytask),....
then place setstate() txt=''; like this
setState(() { txt='' }
Best way to do this is to simply make a widget. For this one, here is the code that i would use
class StateTextField extends StatelessWidget {
final String text;
const StateTextField({Key key, this.text = ''}) : super(key: key);
#override
Widget build(BuildContext context) {
return TextField(
controller: TextEditingController(text: text),
);
}
}
that should do the work. If u want to pass the controller, u can pass it too. And to use it, just call the class in build method.
Similar example with TextFormField, TextEditingController, clickable suffixIcon for a selectDate showDatePicker (this is a standard material "lambda" dialog) where setState is updating the initial set date (not by the initialValue, rather by the controller parameter) with a new date.
import 'package:flutter/material.dart';
class CreateOrderForm extends StatefulWidget {
const CreateOrderForm({Key? key}):super(key: key);
#override
CreateOrderFormState createState() => CreateOrderFormState();
}
class CreateOrderFormState extends State<CreateOrderForm>
{
final _formKey = GlobalKey<FormState>();
late DateTime selectedDate;
#override
void initState() {
super.initState();
selectedDate = DateTime.now();
print("initState selectedDate = ${selectedDate.toLocal()}");
}
#override
Widget build(BuildContext context) {
print("build prepare selectedDate: ${selectedDate.toLocal()}");
return Form(
key: _formKey,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
//initialValue: "${selectedDate.toLocal()}",
controller: TextEditingController(text: "${selectedDate.toLocal()}"),
decoration: InputDecoration(
icon: Icon(Icons.event),
border: OutlineInputBorder(),
labelText: "Assignment Date & Time",
suffixIcon: IconButton(
onPressed: () => _selectDate(context),
icon: Icon(Icons.event))
),
),
const Padding(padding: EdgeInsets.only(top: 8)),
Align(
alignment: Alignment.bottomRight,
child: ElevatedButton(
onPressed: ((){}),
child: Text("Submit")),
)
],
),
),
)
);
}
_selectDate(BuildContext context) async{
await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(1970),
lastDate: DateTime(9999)
).then((value){
if(value != null && value != selectedDate)
{
this.setState(() {
selectedDate = value;
print("New date ${selectedDate.toLocal()}");
});
}
});
}
}
Say if I have ten Text fields in my flutter application,so I have to make ten Text Editing Controller to control the text fields like to get the text from them. However, it may be possible that I would have hundreds of TextField in my application, so, would I have to 100 TextEditingControllers like in below-mentioned code? If not then what is the best way to solve this?
import 'package:flutter/material.dart';
class TestScreen extends StatefulWidget {
#override
_TestScreenState createState() => _TestScreenState();
}
class _TestScreenState extends State<TestScreen> {
final TextEditingController firstController = TextEditingController();
final TextEditingController secondController = TextEditingController();
final TextEditingController thirdController = TextEditingController();
final TextEditingController fourthController = TextEditingController();
final TextEditingController fifthController = TextEditingController();
final TextEditingController sixthController = TextEditingController();
final TextEditingController seventhController = TextEditingController();
final TextEditingController eightController = TextEditingController();
final TextEditingController ninthController = TextEditingController();
final TextEditingController tenthController = TextEditingController();
#override
Widget build(BuildContext context) {
return Container(
child: Form(
child: Column(
children: <Widget>[
TextFormField(
controller: firstController,
),
TextFormField(
controller: secondController,
),
TextFormField(
controller: thirdController,
),
TextFormField(
controller: fourthController,
),
TextFormField(
controller: fifthController,
),
TextFormField(
controller: sixthController,
),
TextFormField(
controller: seventhController,
),
TextFormField(
controller: eightController,
),
TextFormField(
controller: ninthController,
),
TextFormField(
controller: tenthController,
),
],
),
),
);
}
}
To handle multiple TextFormField in flutter, we can use a widget called Form with only one GlobalKey to check and save all the data in the TextFormField.
I would like to explain it, however, I am sure that reading this flutter cookbook you will understand it much better.
Edit
As I see there are some doubts, I will explain briefly how to save the data, that would be the next step on that cookbook.
We create a Form with a GlobalKey as explained in the
cookbook.
We validate the data using _formKey.currentState.validate()
and the validator property on each TextFormField.
We save the text in each TextFormField using
_formKey.currentState.save() and the onSaved property.
The TextFormField would look like this:
TextFormField(
decoration: const InputDecoration(
hintText: 'What do people call you?',
labelText: 'Name *',
),
onSaved: (String value) {
// Use this code to save the text wherever you need it, a Map, List, db...
},
validator: (String value) {
if (value.contains('#') {
return 'Do not use the # char.';
}
return null;
},
)
I hope this helps you!