How can i get text value from TextField while using a ViewModelProvider? - flutter

I have a form (a bunch of TextField widgets), i get the data from a REST API and load it to the view using a ViewModelProvider.
Now i want to press a button "Save", get the text value of the TextField widgets and pass the data to the REST API.
I searched for how to get the value of the TextField to use it elsewhere and all i found was similar to this :
class _MyCustomFormState extends State<MyCustomForm> {
// Create a TextEditingController as a variable
final myController = TextEditingController();
#override
void dispose() {
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Retrieve Text Input'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
// Set the created TextEditingController as the controller of the Text Field
controller: myController,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Use the text attribute of the TextEditingController variable
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: Icon(Icons.text_fields),
),
);
}
}
(This code snippet was taken from the Flutter website)
My problem is that i use a ViewModelProvider and i need to create the TextEditingController inside the
builder attribute of the ViewModelProvider to set the text that was retreived from the HTTP call.
Here is my current code, i left only one TextField because they are practically the same.
class SettingsView extends StatelessWidget {
const SettingsView({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) => model.getSettings(),
builder: (context, model, child) => Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextField(
decoration: new InputDecoration(
labelText: 'Host',
),
controller: TextEditingController.fromValue(
TextEditingValue(
text: model.settings == null? '' :model.settings.host,
),
),
),
GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
),
],
),
),
),
);
}
void saveData() {
}
}
So the question is, how to pass the text value of the TextField to the saveData method?
Thank you.

Are you trying to init the TextEditingController?
If so, you can initialize the text of the controler on onModelReady, and declare the controler outside of build.
class _SettingsViewState extends State<SettingsView> {
TextEditingController myController = TextEditingController();
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) {
myController.text =
model.settings == null ? '' : model.settings.host;
model.getSettings();
},
builder: (context, model, child) => Center(
child: Column(
children: <Widget>[
Expanded(
child: TextField(
decoration: new InputDecoration(labelText: 'Host'),
controller: myController,
),
),
Expanded(
child: GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
),
),
],
),
),
);
}
void saveData() {
print('save data ${myController.text}');
}
}

Thanks to Thiago Dong Chen i managed to find the solution to my problem.
Thiago suggested that i create a TextEditingController variable and set its text value in onModelReady as follows
onModelReady: (model) {
myController.text = model.settings == null ? '' : model.settings.host;
model.getSettings();
},
The problem is even in onModelReady the model.settings is still null. But his solution put me in the right path. I'm new to Flutter, Dart and the anonymous functions. Seeing that i can change the => to a more classic {} made me realize that i can do the same in builder and do more that just setting the widget tree.
Here is the full code :
class SettingsView extends StatefulWidget {
const SettingsView({Key key}) : super(key: key);
#override
_SettingsViewState createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
TextEditingController _hostController = TextEditingController();
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) => model.getSettings(),
builder: (context, model, child) {
_hostController.text = model.settings == null ? null : model.settings.host;
return Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextField(
decoration: new InputDecoration(
labelText: "Host",
),
controller: _hostController,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
),
SizedBox(
height: 30,
),
GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
)
],
),
),
);
}
);
}
void saveData() async{
var _api = locator<Api>();
bool b = await _api.setSettings(SettingItemModel(
host: _hostController.text));
String message = '';
if (b) {
message = 'Data saved to the database!';
} else {
message = 'Couldn\'t save the data';
}
final snackBar = SnackBar(content: Text(message));
Scaffold.of(context).showSnackBar(snackBar);
}
}

Related

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);
}
:)

"onSaved()" function in TextFormField not being reached (dart/flutter)

I have built a very simple form in flutter and I am trying to save the value of whatever is typed in the form fields to variables. This way, I can push these variables to firebase. However, nothing in the onSaved() block of the TextFormFields is being run. I have called save() on the current state of the form, but it still doesn't seem to work. Any ideas?
I have attached the code for the page below:
import 'package:flutter/material.dart';
class AddJobPage extends StatefulWidget {
const AddJobPage({Key? key}) : super(key: key);
static Future<void> show(BuildContext context) async {
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const AddJobPage(),
fullscreenDialog: true
));
}
#override
_AddJobPageState createState() => _AddJobPageState();
}
class _AddJobPageState extends State<AddJobPage> {
final _formKey = GlobalKey<FormState>();
//These two variables are where we will store the values of the text form fields
//before we push to firestore.
String? _name = '';
int _ratePerHour = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 2,
title: const Text("New Job"),
backgroundColor: Colors.teal.shade700,
actions: [
TextButton(
onPressed: _submit,
child: const Text(
'Save',
style: TextStyle(fontSize: 18, color: Colors.white),
)
)
],
),
body: _buildContents(),
backgroundColor: Colors.grey.shade200,
);
}
void _submit() {
if(_validateAndSave()) {
print("form saved, name: $_name, ratePerHour: $_ratePerHour");
}
}
bool _validateAndSave() {
final form = _formKey.currentState;
if(form!.validate()) {
print("the form was saved here");
form.save;
return true;
}
return false;
}
Widget _buildContents() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildForm(),
),
),
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildFormChildren(),
)
);
}
List<Widget> _buildFormChildren() {
return [
TextFormField(
decoration: const InputDecoration(labelText: 'Job name'),
onSaved: (value) {
print("code doesn't reach here");
_name = value; //save the value of the text field to _name
}
),
TextFormField(
decoration: const InputDecoration(labelText: 'Rate Per Hour'),
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: false
),
onSaved: (value) {
_ratePerHour = int.tryParse(value!) ?? 0;
}
),
];
}
}
You need to call the save function.
So replace form.save; in your code with form.save();
replace form.save with form.save() and you're done. No need to worry about text controllers since you're using a text form field
you need to add a TextEditingController to get the text from a TextFormField. Then you need to call setstate inside OnChanged, not on Onsaved so you can transfer the text to a variable. This is what your code should look like:
First you initialize a controller like this
final TextEditingController textController = TextEditingController();
String _name = "not set";
then you add the controller to your textformfield like this
TextFormField(
controller: textController ,
onChanged:(value)
{
setState(() {
_name = textController.text;
});
} ,
)
This is a complete example:
class TextFormFieldExample extends StatefulWidget {
const TextFormFieldExample({Key? key}) : super(key: key);
#override
_TextFormFieldExampleState createState() => _TextFormFieldExampleState();
}
class _TextFormFieldExampleState extends State<TextFormFieldExample> {
//Create the controller here
final TextEditingController textController = TextEditingController();
String _name = "not set";
#override
Widget build(BuildContext context) {
print(_name);
return Scaffold(
body: Material(
child: Container(
color: Colors.white,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child:TextFormField(
controller: textController ,//Attach the controller to the text form here
onChanged:(value)
{
setState(() {
_name = textController.text;//Save the text from the controller to a variable
});
} ,
),
),
),
);
}
}

Nullable class instance with Provider

I have a class named User which basically has two variables. name and age. Also, I have another class named UserList.The aim of this class is to add the user objects to a list and return that list.
User model
class User with ChangeNotifier{
late String? name;
late int? age;
//set user name;
setName(String name){
this.name=name;
}
//set user age
setAge(int age){
this.age=age;
}
}
UserList class
class UserList with ChangeNotifier{
final List<User> _list=[];
//add a new user to the list.
void addUserToList(User user){
_list.add(user);
notifyListeners();
}
//return the private list of users.
List<User>getUsers() =>_list;
}
Here is the scenario. I want to insert a value from one page and view that inserted value on the second page. Look at the pictures below. On the first page,
I insert the age and name values
Assign those values in a User object instance (user.setName, user.setAge)
Add that user object instance in a UserList list
Use provider to provide that list.(Provider.of<UserList>(context).addUserToList(user);
First Page
First Page Code
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserList()),
ChangeNotifierProvider(create: (context) => User()),
],
child: const MyApp())
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
TextEditingController nameController = TextEditingController();
TextEditingController ageController = TextEditingController();
#override
Widget build(BuildContext context) {
User user =User();
return Scaffold(
backgroundColor: Colors.grey[300],
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
hintText: "Name",
),
),
TextField(
controller: ageController,
decoration: const InputDecoration(
hintText: "Age",
),
),
ElevatedButton(onPressed: (){
//providing userList
user.setName(nameController.text); //setting user name
user.setAge(int.parse(ageController.text)); // setting user age
Provider.of<UserList>(context,listen: false).addUserToList(user); //providing the list of users
nameController.text=""; //after insertion, clearing the text field
ageController.text="";//after insertion, clearing the text field
user=User(); // instantiating a new user object for the following insertion.
Navigator.push(context, MaterialPageRoute(builder: (context)=>const ViewUser()));
}, child: const Text("Submit"))
],
),
),
);
}
}
On the second page, display those inserted values in a ListView. Let's head to the main problem. Each item of ListViewis wrapped with a GestureDetector to be able to gain functionality.I hope everything is clear up to now.
The problem is that; When I click on each item I want to return to the first page. But, this time the TextField shouldn't be empty. It must be replaced with the value of that item. For exmple. If I click on John Nash and 43. The First page must pop up with field value of that list item. To be able to do that I use another Provider to provide User object. But, I couldn't do it because of null safety it gives an error about the user object is null. Is there any idea that could be helpful.
Second Page
Second Page code
class ViewUser extends StatelessWidget {
const ViewUser({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
List<User> userList = Provider.of<UserList>(context).getUsers();
return Scaffold(
appBar: AppBar(
title: const Text("Users List"),
),
body: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: userList.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
const SizedBox(height: 15,),
GestureDetector(
onTap: (){
Provider.of<User>(context,listen: false)
.setName(userList[index].name!);
Provider.of<User>(context,listen: false)
.setAge(userList[index].age!);
Navigator.push(context, MaterialPageRoute(builder: (context)=>MyHomePage()));
},
child: Container(
height: 50,
color: Colors.amber,
child: Center(
child: Row(
children: [
Text("Name: " + userList[index].name!),
const SizedBox(
width: 40,
),
Text("Age:" + userList[index].age.toString()),
],
),
),
),
),
],
);
}),
);
}
}
While you are using Navigator.push you can pass user as arguments.
GestureDetector(
onTap: () {
final user = userList[index];
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
settings: RouteSettings(
arguments: user,
)),
);
},
MyHomePage build method will be
#override
Widget build(BuildContext context) {
User user = User();
User? args = ModalRoute.of(context)?.settings.arguments as User?;
if (args != null) {
nameController.text = args.name ?? "";
ageController.text = args.age.toString();
}
return Scaffold(....
You can also use Navigator.pop in this case.
GestureDetector(
onTap: () {
final user = userList[index];
Navigator.pop(context, user);
},
And to receive data on MyHomePage
onPressed: () async {
//....
final data = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ViewUser(),
),
);
if (data != null) {
nameController.text = data.name ?? "";
ageController.text = data.age.toString();
}
More about navigate-with-arguments.

How do avoid markNeedsBuilder() error using Flutter_Riverpod and TextEditingControllers on TextFormFields?

The form below is using ConsumerWidget from the flutter_riverpod package to watch for updates on first/last name fields in a firebase stream provider. Then using TextEditingControllers I am both setting the watched text values in the fields and also getting the text values when I update the account in Firebase.
This all works great until I change a value in the first or last name fields directly in Firebase, which causes a rebuild in the ui. While the UI does display the update Firebase value I get the following Exception in the run logs.
Appears riverpod is battling with the TextEditingControllers over state, which makes sense, but how do I overcome this?
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for TextEditingController:
setState() or markNeedsBuild() called during build.
This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey#78eaf]
state: FormState#7d070
The widget which was currently being built when the offending call was made was: FirstLastName
dirty
dependencies: [UncontrolledProviderScope]
Can I use the flutter_riverpod package when I am using a Stateful Widget that is required for using TextEditingControllers? Or do I need to look at using the hooks_riverpod package or just riverpod package so that I can use TextEditingControllers to set values in fields and read values from fields?
Code excerpts below:
account_setup.dart
class AccountSetup extends StatefulWidget {
#override
_AccountSetupState createState() => _AccountSetupState();
}
class _AccountSetupState extends State<AccountSetup> {
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
body: Form(
key: _formKey,
child: ListView(
children: [
AccountSettingsTitle(
title: 'Account Setup',
),
FirstLastName(_firstNameController, _lastNameController),
SizedBox(
height: 24.0,
),
],
),
),
),
);
}
}
class FirstLastName extends ConsumerWidget {
FirstLastName(
this.firstNameController,
this.lastNameController,
);
final TextEditingController firstNameController;
final TextEditingController lastNameController;
#override
Widget build(BuildContext context, ScopedReader watch) {
final account = watch(accountStreamProvider);
return account.when(
data: (data) {
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
return Column(
children: [
Center(
child: Padding(
padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
child: TextFormField(
controller: firstNameController,
decoration: kInputStringFields.copyWith(
hintText: 'First Name',
),
autocorrect: false,
validator: (String value) {
if (value.isEmpty) {
return 'Enter first name';
}
return null;
},
),
),
),
SizedBox(
height: 14.0,
),
Center(
child: Padding(
padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
child: TextFormField(
controller: lastNameController,
decoration: kInputStringFields.copyWith(
hintText: 'Last Name',
),
autocorrect: false,
validator: (String value) {
if (value.isEmpty) {
return 'Enter last name';
}
return null;
},
),
),
),
],
);
},
loading: () => Container(),
error: (_, __) => Container(),
);
}
}
top_level_providers.dart
final accountStreamProvider = StreamProvider.autoDispose<Account>((ref) {
final database = ref.watch(databaseProvider);
return database != null ? database.accountStream() : const Stream.empty();
});
The problem is that you trigger rebuild of your widget during its build method execution with this lines:
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
Hovewer, solution is quite simple. Just wrap it with zero-delayed Future:
Future.delayed(Duration.zero, (){
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
});
Basically, always when you see this error, you need to find the code that trigger rebuild during build and wrap it in Future
assertion was thrown while dispatching notifications for
TextEditingController: setState() or markNeedsBuild() called during
build.
This error is shown when you update a CahngeNotifier inside a build method, in this case TextEditingController is updated when you're building the widgets:
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
....
As you mentioned, hooks_riverpod could be an option, but if you don't want to flood yourself with libraries until fully understand riverpod or state management I would recommend 2 approaches:
Try using ProviderListener (part of flutter_riverpod):
class AccountSetup extends StatefulWidget {
#override
_AccountSetupState createState() => _AccountSetupState();
}
class _AccountSetupState extends State<AccountSetup> {
final TextEditingController _firstNameController = TextEditingController();
final TextEditingController _lastNameController = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
body: Form(
key: _formKey,
child: ListView(
children: [
AccountSettingsTitle(
title: 'Account Setup',
),
ProviderListener<AsyncValue>(
provider: accountStreamProvider,
onChange: (context, account) { //This will called when accountStreamProvider updates and a frame after the widget rebuilt
if(account is AsyncData) {
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
}
},
child: FirstLastName(_firstNameController, _lastNameController),
),
SizedBox(
height: 24.0,
),
],
),
),
),
);
}
}
Or you can use it inside FirstLastName and wrap the widget result, it should work the same (remember to delete the lines firstNameController.text = data.firstName; and lastNameController.text = data.lastName; inside when.data to prevent the error)
#override
Widget build(BuildContext context, ScopedReader watch) {
final account = watch(accountStreamProvider);
return ProviderListener<AsyncValue>(
provider: accountStreamProvider,
onChange: (context, account) { //This will called when accountStreamProvider updates and a frame after the widget rebuilt
if(account is AsyncData) {
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
}
},
child: account.maybeWhen(
data: (data) {
/// don't call firstNameController.text = data.firstName here
return Column(
children: [
....
],
);
},
orElse: () => Container(),
),
);
}
}
The other option is to create your own TextEditingController using riverpod and update it with the data of the stream when its created:
final firstNameProvider = ChangeNotifierProvider.autoDispose<TextEditingController>((ref) {
final account = ref.watch(accountStreamProvider);
final String name = account.maybeWhen(
data: (data) => data?.firstName,
orElse: () => null,
);
return TextEditingController(text: name);
});
final lastNameProvider = ChangeNotifierProvider.autoDispose<TextEditingController>((ref) {
final account = ref.watch(accountStreamProvider);
final String lastName = account.maybeWhen(
data: (data) => data?.lastName,
orElse: () => null,
);
return TextEditingController(text: lastName);
});
Then instead of creating them in the parent StatefulWidget just call it from the consumer in FirstLastName(); (there is no need to pass TextEditingControllers in the constructor anymore)
class FirstLastName extends ConsumerWidget {
const FirstLastName({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final account = watch(accountStreamProvider);
return account.maybeWhen(
data: (data) {
return Column(
children: [
Center(
child: Padding(
padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
child: Consumer(
builder: (context, watch, child) {
final firstNameController = watch(firstNameProvider); //call it here
return TextFormField(
controller: firstNameController,
decoration: kInputStringFields.copyWith(
hintText: 'First Name',
),
autocorrect: false,
validator: (String value) {
if (value.isEmpty) {
return 'Enter first name';
}
return null;
},
);
}
),
),
),
SizedBox(
height: 14.0,
),
Center(
child: Padding(
padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
child: child: Consumer(
builder: (context, watch, child) {
final lastNameController = watch(lastNameProvider); //call it here
return TextFormField(
controller: lastNameController ,
decoration: kInputStringFields.copyWith(
hintText: 'LAst Name',
),
autocorrect: false,
validator: (String value) {
if (value.isEmpty) {
return 'Enter first name';
}
return null;
},
);
}
),
),
),
],
);
},
orElse: () => Container(),
);
}
}

Multiple widgets used the same globalkey error in flutter

I tried to solve this, I looked up the answers in Stack Overflow
But I haven't solved it yet
I used the global key in the create and update pages
What I've done
I tried adding static to the global key ,but I couldn't
because I couldn't wrap the key in a refreshIndicator.
I used Navigator pushNamed instead of Navigator push
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class Update extends StatefulWidget {
#override
_UpdateState createState() => _UpdateState();
}
class _UpdateState extends State<Update> {
GlobalKey<FormState> _formKey1 = GlobalKey<FormState>(debugLabel: '_updateFormKey');
TextEditingController _titleController1 = TextEditingController();
TextEditingController _descController1 = TextEditingController();
final db = Firestore.instance;
DocumentSnapshot _currentDocument;
#override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text('update'),
),
body: _buildUpdate(context)));
}
Widget _buildUpdate(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return StreamBuilder<QuerySnapshot>(
stream: db.collection('flutter_data2').snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: snapshot.data.documents.map<Widget>((doc) {
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(20.0),
child: Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Form(
key: _formKey1,
child: Padding(
padding: EdgeInsets.only(left: 12, right: 12),
child: Column(
children: <Widget>[
TextFormField(
controller: _titleController1,
decoration: InputDecoration(labelText: doc.data['title']),
validator: (String value) {
if (value.isEmpty) {
return 'title empty';
} else {
return null;
}
},
),
TextFormField(
controller: _descController1,
decoration: InputDecoration(labelText: doc.data['desc']),
validator: (String value) {
if (value.isEmpty) {
return 'desc empty';
} else {
return null;
}
},
),
],
),
),
),
),
),
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0)),
child: Text('update'),
color: Colors.blue,
onPressed: () async {
if (_formKey1.currentState.validate()) {
db
.collection('flutter_data2')
.document(doc.documentID)
.updateData({'title': _titleController1.text,'desc':_descController1.text});
Navigator.pop(context);
}
},
),
],
);
}).toList(),
);
} else {
return SizedBox();
}
},
);
}
}
You might really want to use some modularity here. Create your custom Form widget preferably in a different file with their own set of controllers. This way you won't have to manage the controllers explicitly. One more thing to notice is that your Button is doing the same job for every entry. In this case, you might as well add the global key inside your custom Form widget and hardcode the onPressed function there.
Here is an example
// This is a mock data. Your firebase snapshot.data will have a similar structure
List<Map<String, dynamic>> _mockData = [
{
'title':'Title 1',
'desc':'Description 1',
},
{
'title':'Title 2',
'desc':'Description 2',
},
{
'title':'Title 3',
'desc':'Description 3',
},
{
'title':'Title 4',
'desc':'Description 4',
},
];
// There are many ways to make this work.
// Instead of hardcoding the function in our custom form widget, We would like to pass a function implementation which will be called after the button in the form is pressed. This way we will have more control on what will happen when we press the button
typedef onFormData = Future<void> Function(String, String); // Future void to allow async updates // The two strings are title and description respectively.
// This is the custom form widget you need to create
class MyForm extends StatefulWidget {
final Map<String, dynamic> data; // Replace it with DocumentSnapshot data.
final onFormData onPressed; // We will use the type we defined up there. So we will be expecting a function implementation here which takes two strings, a title and a description
MyForm({#required this.data, #required this.onPressed, Key key}):super(key: key);
#override
createState() => _MyFormState();
}
// Our custom form widget is defined here
class _MyFormState extends State<MyForm> {
// Define the controllers
TextEditingController _titleController;
TextEditingController _descController;
// Create the key
GlobalKey<FormState> _formKey;
#override
void initState() {
// Initialize the values here
super.initState();
_titleController = TextEditingController();
_descController = TextEditingController();
_formKey = GlobalKey<FormState>();
}
#override
void dispose() {
// Remember that you have to dispose of the controllers once the widget is ready to be disposed of
_titleController.dispose();
_descController.dispose();
_formKey = null;
super.dispose();
}
#override
Widget build(BuildContext context) {
// Everything remains almost same here as in your code
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(20.0),
child: Card(
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.only(left: 12, right: 12),
child: Column(
children: <Widget>[
TextFormField(
controller: _titleController, // Assign the controller
decoration:
InputDecoration(labelText: widget.data['title']), // widget.data can still be indexed like this after you replace datatype of the data to DocumentSnapshot
validator: (String value) {
if (value.isEmpty) {
return 'title is empty';
} else {
return null;
}
},
),
TextFormField(
controller: _descController,
decoration:
InputDecoration(labelText: widget.data['desc']), // Same goes here
validator: (String value) {
if (value.isEmpty) {
return 'description is empty';
} else {
return null;
}
},
),
],
),
),
),
),
),
// The button associated with this form
RaisedButton(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
child: Text('Update'),
color: Colors.blue,
onPressed: () async {
// If validation is successful, then call the on pressed function we assigned to the widget. // Check the MyWidget class
if (_formKey.currentState.validate()) {
await widget.onPressed(_titleController.text, _descController.text); // Instead of putting firebase update code here, we are passing the title and description to our main widget from where we will post
}
},
),
],
);
}
}
// Our main widget
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Demo'),
),
// Wrap this up in your stream builder
// I am using a listview with mock data for the sake of this example.
body: ListView.builder(
itemBuilder: (context, index) {
// We create a new instance of our custom form and we don't need to manage any controllers or keys. We just need to pass the data and what happens when we press the update button in our custom form.
// Here is why we defined a type named onFormData before.
// You can simply post updates in your form widget directly if your logic is same for each form
// We are getting the title and description info here through our custom defined Forms without managing any keys and controllers.
// Also this method is async so you can post your firebase updates from here waiting for them to complete using await
return MyForm(data: _mockData[index], onPressed: (String title, String description) async {
// Put your firebase update code here
_mockData[index]['title'] = title;
_mockData[index]['desc'] = description;
Navigator.of(context).pop(); // Go back after the updates are made as written in your example
});
},
physics: BouncingScrollPhysics(),
itemCount: _mockData.length, // Length of the data.
),
);
}
}
Before any updates:
After writing your title and description:
After pressing update, when you go back to the same screen:
Hope this helps!