"onSaved()" function in TextFormField not being reached (dart/flutter) - 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
});
} ,
),
),
),
);
}
}

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

How to disable button until Flutter text form field has valid data

I want to disable a button until the text form field is valid. And then once the data is valid the button should be enabled. I have received help on SO previously with a similar question but can't seem to apply what I learned to this problem. The data is valid when the user adds more than 3 characters and fewer than 20. I created a bool (_isValidated) and added it to the validateUserName method calling setState once the user has entered valid data but this is definitely wrong and generates an error message. The error message is:
setState() or markNeedsBuild() called during build.
I can't figure out what I am doing wrong. Any help would be appreciated. Thank you.
class CreateUserNamePage extends StatefulWidget {
const CreateUserNamePage({
Key? key,
}) : super(key: key);
#override
_CreateUserNamePageState createState() => _CreateUserNamePageState();
}
class _CreateUserNamePageState extends State<CreateUserNamePage> {
final _formKey = GlobalKey<FormState>();
bool _isValidated = false;
late String userName;
final TextEditingController _userNameController = TextEditingController();
#override
void initState() {
super.initState();
_userNameController.addListener(() {
setState(() {});
});
}
void _clearUserNameTextField() {
setState(() {
_userNameController.clear();
});
}
String? _validateUserName(value) {
if (value!.isEmpty) {
return ValidatorString.userNameRequired;
} else if (value.trim().length < 3) {
return ValidatorString.userNameTooShort;
} else if (value.trim().length > 20) {
return ValidatorString.userNameTooLong;
} else {
setState(() {
_isValidated = true;
});
return null;
}
}
void _createNewUserName() {
final form = _formKey.currentState;
if (form!.validate()) {
form.save();
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Welcome $userName'),
),
);
Timer(const Duration(seconds: 2), () {
Navigator.pop(context, userName);
});
}
#override
void dispose() {
_userNameController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final isPortrait =
MediaQuery.of(context).orientation == Orientation.portrait;
final screenHeight = MediaQuery.of(context).size.height;
return WillPopScope(
onWillPop: () async => false,
child: Scaffold(
appBar: CreateUserNameAppBar(
preferredAppBarSize:
isPortrait ? screenHeight / 15.07 : screenHeight / 6.96,
),
body: ListView(
children: [
Column(
children: [
const CreateUserNamePageHeading(),
CreateUserNameTextFieldTwo(
userNameController: _userNameController,
createUserFormKey: _formKey,
onSaved: (value) => userName = value as String,
suffixIcon: _userNameController.text.isEmpty
? const EmptyContainer()
: ClearTextFieldIconButton(
onPressed: _clearUserNameTextField,
),
validator: _validateUserName,
),
CreateUserNameButton(
buttonText: ButtonString.createUserName,
onPressed: _isValidated ? _createNewUserName : null,
),
],
),
],
),
),
);
}
}
Simply use form validation, inside TextFormField edit validator function , add onChange function and call setState to get inputtedValue that can also keep disable button unless the form is validated.
Key points to note:
Use final _formKey = GlobalKey<FormState>();
The String? inputtedValue; and !userInteracts() are the tricks, you can refer to the code;
When ElevatedButton onPressed method is null, the button will be disabled. Just pass the condition !userInteracts() || _formKey.currentState == null || !_formKey.currentState!.validate()
Code here:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyCustomForm(),
);
}
}
class MyCustomForm extends StatefulWidget {
const MyCustomForm({Key? key}) : super(key: key);
#override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
// Create a corresponding State class.
// This class holds data related to the form.
class MyCustomFormState extends State<MyCustomForm> {
// Create a global key that uniquely identifies the Form widget
// and allows validation of the form.
//
// Note: This is a GlobalKey<FormState>,
// not a GlobalKey<MyCustomFormState>.
final _formKey = GlobalKey<FormState>();
// recording fieldInput
String? inputtedValue;
// you can add more fields if needed
bool userInteracts() => inputtedValue != null;
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Scaffold(
appBar: AppBar(
title: const Text('Form Disable Button Demo'),
),
body: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (inputtedValue != null && (value == null || value.isEmpty)) {
return 'Please enter some text';
}
return null;
},
onChanged: (value) => setState(() => inputtedValue = value),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
// return null will disable the button
// Validate returns true if the form is valid, or false otherwise.
onPressed: !userInteracts() || _formKey.currentState == null || !_formKey.currentState!.validate() ? null :() {
// If the form is valid, display a snackbar. In the real world,
// you'd often call a server or save the information in a database.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Processing Data: ' + inputtedValue!)),
);
},
child: const Text('Submit'),
),
),
],
),
),
);
}
}
I think the better way is to assign a null value to the onPressed parameter of the button. Please check the below link.
https://www.flutterbeads.com/disable-button-in-flutter/
You have custom widgets, so I don't know how does your widgets works bu here you can use AbsorbPointer to disable a button and check your textformfield text in onChange parameter like here;
bool isDisabled = true;
String _validateUserName(value) {
if (value!.isEmpty) {
return ValidatorString.userNameRequired;
} else if (value.trim().length < 3) {
return ValidatorString.userNameTooShort;
} else if (value.trim().length > 20) {
return ValidatorString.userNameTooLong;
} else {
setState(() {
isDisabled = false;
});
return null;
}
}
#override
Widget build(BuildContext context) {
final ButtonStyle style =
ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20));
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ElevatedButton(
style: style,
onPressed: null,
child: const Text('Disabled'),
),
const SizedBox(height: 30),
TextFormField(
decoration: const InputDecoration(
labelText: 'Label',
),
onChanged: (String value) {
_validateUserName(value);
},
),
AbsorbPointer(
absorbing: isDisabled, // by default is true
child: TextButton(
onPressed: () {},
child: Text("Button Click!!!"),
),
),
],
),
);
}

Flutter Barcode Scan Result Is'nt Listed

My application is to search through the list of books. Two different variables (book name or barcode) can be used while searching. There is no problem when searching by name. but when searching with barcode scanning, no results are listed. When I type the barcode manually, the application still works without any problems.
Can u help me?
Manually entered barcode: https://i.stack.imgur.com/njtLA.png
Barcode scan result : https://i.stack.imgur.com/ZsGot.png
My code here..
import 'package:fff/book_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:fff/book_model.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController _controller = new TextEditingController();
List<Book> _booksForDisplay = [];
List<Book> _books = [];
#override
void initState() {
super.initState();
fetchBooks().then((value) {
setState(() {
_books.addAll(value);
_booksForDisplay = _books;
print(_booksForDisplay.length);
});
});
}
Future _scan(BuildContext context) async {
String barcode = await FlutterBarcodeScanner.scanBarcode(
'#ff0000',
'İptal',
true,
ScanMode.BARCODE
);
_controller.text = barcode;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 80,
title: Padding(
padding: EdgeInsets.all(8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(40)
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: TextFormField(
textAlignVertical: TextAlignVertical.center,
controller: _controller,
decoration: InputDecoration(
border: InputBorder.none,
icon: Icon(Icons.search),
suffixIcon: IconButton(
icon: Icon(FontAwesomeIcons.barcode),
onPressed: (){
_scan(context);
},
)
),
onChanged: (string){
string = string.toLowerCase();
setState(() {
_booksForDisplay = _books.where((b){
var bName = b.name!.toLowerCase();
var bBarcode = b.barcode!.toLowerCase();
return bName.startsWith(string) || bBarcode.startsWith(string);
}).toList();
});
},
),
),
),
),
),
body: SafeArea(
child: Container(
child: _controller.text.isNotEmpty ? new ListView.builder(
itemCount: _booksForDisplay.length,
itemBuilder: (context, index){
return BookTile(book: this._booksForDisplay[index]);
},
)
:
Center(
child: Text('Searching..'),
)
),
)
);
}
}
I think you only need a listener for your TextEditingController. And you should write your onChanged method inside that listener.
#override
void initState() {
super.initState();
fetchBooks().then((value) {
setState(() {
_books.addAll(value);
_booksForDisplay = _books;
print(_booksForDisplay.length);
});
});
_controller.addListener(() {
print(_controller.text);
var string = _controller.text.toLowerCase();
setState(() {
_booksForDisplay = _books.where((b){
var bName = b.name!.toLowerCase();
var bBarcode = b.barcode!.toLowerCase();
return bName.startsWith(string) ||
bBarcode.startsWith(string);
}).toList();
});
});
}

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!

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

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