Manually setting Flutter Validation Error - forms

After validating a form and sending a request from flutter to the server backend: I want to set any potential error message from the server to be displayed in the original form. Preferably exactly like a validation error.
For instance:
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
// this is where I want to set the "validation" error
}
}

It's actually super simple and the validation error still works aswell.
String? _errorMsg;
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
// will set the errorText directly, no need for a variable here
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
errorText: _errorMsg,
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
setState(() {
_errorMsg = null; // clear any existing errors
});
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
setState(() {
_errorMsg = 'Wrong password.';
});
}
}

I suppose, I could think of a solution, but I think it's kind of ugly.
I could have an "error" variable, that is set when the request fails.
I would then call formState.validate() a second time, in there: check the error variable and return it if it's not null.

You can use flutter_form_bloc and use addError method of TextFieldBloc.
usernameField.addError('That username is taken. Try another.');
Keep in mind that you can also use asynchronous validators.
This is a complete example:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^0.21.0
form_bloc: ^0.5.0
flutter_form_bloc: ^0.4.1+1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
import 'package:form_bloc/form_bloc.dart';
void main() {
runApp(MaterialApp(home: SignUpForm()));
}
class SignUpFormBloc extends FormBloc<String, String> {
final usernameField = TextFieldBloc();
final passwordField =
TextFieldBloc(validators: [Validators.passwordMin6Chars]);
#override
List<FieldBloc> get fieldBlocs => [usernameField, passwordField];
#override
Stream<FormBlocState<String, String>> onSubmitting() async* {
// Form logic...
try {
await _signUp(
throwException: true,
username: usernameField.value,
password: passwordField.value,
);
yield currentState.toSuccess();
} catch (e) {
// When get the error from the backend you can
// add the error to the field:
usernameField.addError('That username is taken. Try another.');
yield currentState
.toFailure('The error was added to the username field.');
}
}
Future<void> _signUp({
#required bool throwException,
#required String username,
#required String password,
}) async {
print(username);
print(password);
await Future<void>.delayed(Duration(seconds: 2));
if (throwException) throw Exception();
}
}
class SignUpForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<SignUpFormBloc>(
builder: (context) => SignUpFormBloc(),
child: Builder(
builder: (context) {
final formBloc = BlocProvider.of<SignUpFormBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Sign Up Form')),
body: FormBlocListener<SignUpFormBloc, String, String>(
onSubmitting: (context, state) {
// Show the progress dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Center(
child: Card(
child: Container(
width: 80,
height: 80,
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(),
),
),
),
),
);
},
onSuccess: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Navigate to success screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SuccessScreen()));
},
onFailure: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Show snackbar with the error
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.failureResponse),
backgroundColor: Colors.red[300],
),
);
},
child: ListView(
children: <Widget>[
TextFieldBlocBuilder(
textFieldBloc: formBloc.usernameField,
decoration: InputDecoration(labelText: 'Username'),
),
TextFieldBlocBuilder(
textFieldBloc: formBloc.passwordField,
decoration: InputDecoration(labelText: 'Password'),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: formBloc.submit,
child: Center(child: Text('SUBMIT')),
),
),
],
),
),
);
},
),
);
}
}
class SuccessScreen extends StatelessWidget {
const SuccessScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green[300],
body: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Icon(
Icons.sentiment_satisfied,
size: 100,
),
RaisedButton(
color: Colors.green[100],
child: Text('Sign out'),
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SignUpForm())),
)
],
),
),
),
);
}
}

A simple solution:
Make a key for the widgets state:
GlobalKey<_CustomErrorTextField> _passwordTextFieldState = GlobalKey();
Set the Error message using the key:
_passwordTextFieldState.currentState.updateError(errorMsg);
Reset the error after 2 seconds:
Future.delayed(Duration(seconds: 2), () {
// Runs after duration sec
_passwordTextFieldState.currentState.updateError(null);
});
Set up the widget (be sure to set the key):
CustomErrorTextField(
key: _passwordTextFieldState,
label: "Password",
currentPassword: password,
validator: yourValidator,
callback: passwordCallback,
obscureText: hidePassword.value - a bool value show/hide password
)
Here is the Widget:
class CustomErrorTextField extends StatefulWidget {
CustomErrorTextField({
Key key,
this.label,
this.currentPassword,
this.validator,
this.callback,
this.obscureText = false
}): super(key: key);
final String label;
final String currentPassword;
final FormFieldValidator<String> validator;
final Function callback;
final obscureText;
#override
_CustomErrorTextField createState() => _CustomErrorTextField();
}
class _CustomErrorTextField extends State<CustomErrorTextField> {
String errorMsg;
updateError(String errorMsg){
setState(() {
this.errorMsg = errorMsg;
});
}
#override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: widget.label,
errorText: errorMsg
),
initialValue: widget.currentPassword,
keyboardType: TextInputType.visiblePassword,
validator: widget.validator,
onSaved: (String val) {
widget.callback(val);
},
obscureText: widget.obscureText,
);
}
}

Related

init state for FutureBuilder flutter

i am confused where can i implement the init state condition for future builder. see whats wrong in my code. in flutter documentation is refer initState for Future builder widget.
` #override
void initState() {
super.initState();
futureLoginuser = loginuser();
}`
i am trying to navigate new screen after response data arrives.my complete code is here i am using go_router for navigation.
class LoginForm extends StatefulWidget {
const LoginForm({Key? key}) : super(key: key);
#override
LoginFormState createState() {
return LoginFormState();
}
}
class LoginFormState extends State<LoginForm> {
TextEditingController mobileController = TextEditingController();
TextEditingController passwordController = TextEditingController();
final _mobileKey = GlobalKey<FormState>();
final _passwordKey = GlobalKey<FormState>();
get message => null;
get messagecode => null;
get userinfo => null;
get token => null;
Future<Loginuser> loginuser(String mobile, String password) async {
final response = await http.post(
Uri.parse('https:random.url/api/login'),
body: {'mobile': mobile, 'password': password});
if (response.statusCode == 200) {
return Loginuser.fromJson(jsonDecode(response.body));
}
} else {
throw Exception('Failed to update');
}
#override
Widget build(BuildContext context) {
return Form(
key: _mobileKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
TextFormField(
controller: mobileController,
autofocus: true,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: "Enter Your Mobile Number",
),
),
TextFormField(
controller: passwordController,
key: _passwordKey,
keyboardType: TextInputType.visiblePassword,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: "Enter Your Password",
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
FutureBuilder<Loginuser>(
future: loginuser(mobileController.text.toString(),
passwordController.text.toString()),
builder: (context, snapshot) {
if (snapshot.hasData) {
print('snapshsot.hasdata');
context.go('/Home');
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
);
}
child: const Text('Submit')),
),
]));
}
}
`
You are using the FutureBuilder wrong it is a Widget that builds itself based on the latest snapshot of interaction with a Future. Its usually used to build widgets that need input when a certain future is completed.
In your case use this:
//first make the onPressed function async
child ElevatedButton(
child: Container(),
onPressed: () async {
// then await the future You want to complete and then use `.then()`
//method to implement the code that you want to implement when the future is completed
await loginuser(mobileController.text.toString(),
passwordController.text.toString())
.then((result) {
print('future completed');
context.go('/Home');
// For errors use onError to show or check the errors.
}).onError((error, stackTrace) {
print(error);
});
});
And do validate the form for any error with form key validation method.
the only way to wait for the future to complete and do something is to use the Asynchronous function either directly as I have shown above or by using the try/catch method both will work fine.
Try this
LoginUser? loginUser
#override
void initState() async{
super.initState();
futureLoginuser = await loginuser();
... // 👈 Your navigation here
}`
Try to make your build responsive by checking for loginUser
#override
Widget build(BuildContext context) {
futureLoginuser == null ?
CircularProgressIndicator() : <Rest of your widget>
}
The way you are trying to implement is not correct, here is very basic example to do
class LoginForm extends StatefulWidget {
const LoginForm({Key? key}) : super(key: key);
#override
LoginFormState createState() {
return LoginFormState();
}
}
class LoginFormState extends State<LoginForm> {
TextEditingController mobileController = TextEditingController();
TextEditingController passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
// your other variables
bool isValidating = false;
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
TextFormField(
controller: mobileController,
autofocus: true,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: "Enter Your Mobile Number",
),
),
TextFormField(
controller: passwordController,
keyboardType: TextInputType.visiblePassword,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: "Enter Your Password",
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: isValidating
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
setState(() {
isValidating = !isValidating;
});
final r = await loginuser(
mobileController.text.toString(),
passwordController.text.toString());
if (r != null) {
// save user state locally, using hive or what alternative you want
context.go('/Home');
} else {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text('Failed'),
));
}
setState(() {
isValidating = !isValidating;
});
}
},
child: const Text('Submit')),
),
]));
}
Future<Loginuser?> loginuser(String mobile, String password) async {
final response = await http.post(Uri.parse('https:random.url/api/login'),
body: {'mobile': mobile, 'password': password});
if (response.statusCode == 200) {
return Loginuser.fromJson(jsonDecode(response.body));
}
return null;
}
}

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

Unable to transfer a variable into another class

In my program, users can create posts. And every post is called stuff. When an user created a stuff, automatically a stuffID assigns to that stuff. Also, I can list those stuffs in user's profile. When a user presses the edit button of one of their stuffs, a form appears in the screen. So the thing i want to do is when user press the edit button, I want to pass the stuffID from profile_sheet.dart class to update_stuff_form.dart class, so I can know to update the which stuff i will.
I have two .dart file. I used this function in my edit button in profile_sheet.dart file:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => updateStuffForm(
stuffID: data['stuffID'].toString())));
And I added my key function into update_stuff_form.dart like so:
class updateStuffForm extends StatefulWidget {
//const updateStuffForm({Key? key}) : super(key: key);
updateStuffForm({Key? key, this.stuffID}) : super(key: key);
final String? stuffID;
#override
_updateStuffFormState createState() => _updateStuffFormState();
}
And I called stuffID from end of the update_stuff_form.dart like this:
widget.stuffID
But whenever I run the program and press the edit button, It gaves me error below:
ErrorSummary('No Material widget found.'),
ErrorDescription(
'${context.widget.runtimeType} widgets require a Material '
'widget ancestor.\n'
'In material design, most widgets are conceptually "printed" on '
"a sheet of material. In Flutter's material library, that "
'material is represented by the Material widget. It is the '
'Material widget that renders ink splashes, for instance. '
'Because of this, many material library widgets require that '
'there be a Material widget in the tree above them.',
),
ErrorHint(
'To introduce a Material widget, you can either directly '
'include one, or use a widget that contains Material itself, '
'such as a Card, Dialog, Drawer, or Scaffold.',
),
Here is my full profile_sheet.dart file:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:unistuff_main/screens/home/update_stuff_form.dart';
class profileSheet extends StatefulWidget {
const profileSheet({Key? key}) : super(key: key);
#override
_profileSheetState createState() => _profileSheetState();
}
class _profileSheetState extends State<profileSheet> {
final _formkey = GlobalKey<FormState>();
//funcs
#override
Widget build(BuildContext context) {
return Container(
child: userStuffList(),
);
}
}
class userStuffList extends StatelessWidget {
const userStuffList({Key? key}) : super(key: key);
void _editStuff(BuildContext context) {
//stuff_form link.
showModalBottomSheet<dynamic>(
isScrollControlled: true,
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0),
child: updateStuffForm());
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _userStuffList(),
);
}
}
class _userStuffList extends StatelessWidget {
void _editStuff(BuildContext context) {
//stuff_form'a yönlendirme.
showModalBottomSheet<dynamic>(
isScrollControlled: true,
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0),
child: updateStuffForm());
});
}
#override
Widget build(BuildContext context) {
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
final _uid = user!.uid;
Query _stuffStream = FirebaseFirestore.instance
.collection('Stuffs')
.where('userID', isEqualTo: _uid);
return Material(
child: StreamBuilder<QuerySnapshot>(
//veri akışı başlatılıyor
//akış oluşturuluyor
stream: _stuffStream.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something is wrong.');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
//showing the data
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
String stuffID = data['stuffID'];
return ListTile(
title: Text(data['title']),
subtitle: Column(
children: <Widget>[
Text(data['details']),
TextButton(
child: const Text('Düzenle'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => updateStuffForm(
stuffID: data['stuffID'].toString())));
_editStuff(context);
})
],
),
);
}).toList(),
);
}),
);
}
}
And my full update_stuff_form.dart (My key reference at first class, my widget.stuffID call all end of it.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
//import 'package:unistuff_main/screens/home/profile_sheet.dart';
class updateStuffForm extends StatefulWidget {
//const updateStuffForm({Key? key}) : super(key: key);
updateStuffForm({Key? key, this.stuffID}) : super(key: key);
final String? stuffID;
#override
_updateStuffFormState createState() => _updateStuffFormState();
}
class _updateStuffFormState extends State<updateStuffForm> {
final _formkey = GlobalKey<FormState>();
final List<String> categories = ["Spor", "Vasıta", "Elektronik"];
editStuff(stuffID) {
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
final _uid = user!.uid;
FirebaseFirestore.instance.collection('Stuffs').doc(stuffID).update({
'title': _currentTitle,
'details': _currentDetails,
'price': _currentPrice,
'category': _currentCategory,
'userID': _uid
});
}
bool validatePrice(String str) {
RegExp _numeric = RegExp(r'^-?[0-9]+$');
return _numeric.hasMatch(str);
}
//form values
String? _currentTitle;
String? _currentDetails;
String? _currentPrice;
String? _currentCategory;
#override
Widget build(BuildContext context) {
return Form(
key: _formkey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text(
'Edit your stuff',
style: TextStyle(fontSize: 18.0),
),
SizedBox(height: 20.0),
TextFormField(
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Title', filled: true),
validator: (val) {
if ((validatePrice(val!) == false)) {
return "Please enter a number";
}
if (val.isEmpty == true) {
return "Please enter a value";
}
return null;
},
onChanged: (val) => setState(() => _currentTitle = val),
),
SizedBox(height: 20.0),
TextFormField(
keyboardType: TextInputType.multiline,
minLines: 1, //Normal textInputField will be displayed
maxLines: 5,
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Details', filled: true),
validator: (val) =>
val!.isEmpty ? 'Please add a detail' : null,
onChanged: (val) => setState(() => _currentDetails = val),
),
SizedBox(height: 20.0),
TextFormField(
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Price', filled: true),
validator: (val) =>
val!.isEmpty ? 'Please enter a price' : null,
onChanged: (val) => setState(() => _currentPrice = val),
),
SizedBox(height: 20.0),
DropdownButtonFormField(
items: categories.map((category) {
return DropdownMenuItem(
value: category,
child: Text('$category'),
);
}).toList(),
onChanged: (val) =>
setState(() => _currentCategory = val as String?),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.pink[400],
),
child: Text('Ekle', style: TextStyle(color: Colors.white)),
onPressed: () async {
editStuff(widget.stuffID);
},
),
],
),
),
);
}
}
With the information about screens provided, I can see that your profile_sheet.dart is not having MaterialApp() as the first ancestor. So in place of Material(), use MaterialApp().
Inside build method of profile_sheet.dart, replace Material() to:
return MaterialApp()

optionsViewBuilder is not called in Autocomplete after fetching results from server

I'm trying to make an auto suggest input which fetches the results from a backend API. Here is my code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hello_world/api.dart';
import 'package:hello_world/autocomplete_item.dart';
class Debouncer {
final int milliseconds;
VoidCallback? action;
Timer? _timer;
Debouncer({this.milliseconds = 250});
run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
class AutoCompleteInput extends StatefulWidget {
const AutoCompleteInput({
Key? key,
this.label = 'Suggest',
this.textInputAction,
this.validator,
this.errorMessage,
}) : super(key: key);
final String label;
final TextInputAction? textInputAction;
final FormFieldValidator<String>? validator;
final String? errorMessage;
#override
_AutoCompleteInputState createState() => _AutoCompleteInputState();
}
class _AutoCompleteInputState extends State<AutoCompleteInput> {
final _debouncer = Debouncer(milliseconds: 500);
List<AutoCompleteItem> _options = [];
bool _isLoading = false;
#override
Widget build(BuildContext context) {
return Autocomplete<AutoCompleteItem>(
displayStringForOption: (AutoCompleteItem item) => item.value,
optionsBuilder: _optionsBuilder,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return TextFormField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
autocorrect: false,
maxLength: 50,
maxLines: 1,
keyboardType: TextInputType.text,
textInputAction: widget.textInputAction,
decoration: InputDecoration(
labelText: widget.label,
contentPadding: EdgeInsets.zero,
errorText: widget.errorMessage,
counterText: '',
suffix: _isLoading
? Padding(
padding: const EdgeInsets.only(right: 1),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
),
validator: widget.validator,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
onChanged: (text) {
_debouncer.run(() async {
setState(() => _isLoading = true);
await _fetchResults(text);
setState(() => _isLoading = false);
});
},
);
},
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<AutoCompleteItem> onSelected,
Iterable<AutoCompleteItem> options,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 2,
child: Container(
width: 300,
constraints: BoxConstraints(maxHeight: 250),
child: ListView.builder(
padding: EdgeInsets.all(10.0),
itemCount: options.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final AutoCompleteItem option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
dense: true,
title: Text(option.value),
),
);
},
),
),
),
);
},
onSelected: (AutoCompleteItem selection) {
print('${selection.type} => ${selection.value}');
},
);
}
Future<void> _fetchResults(String text) async {
// WE PERFORM THE HTTP REQUEST HERE AND THEN ASSIGN THE RESPONSE TO `_options`
setState(() async {
_options = await Api.fetchSuggestions(text);
});
}
Iterable<AutoCompleteItem> _optionsBuilder(
TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<AutoCompleteItem>.empty();
}
return _options;
}
}
As you can see, I call _fetchResults method (which fetches the data from the API) inside the onChanged method of TextFormField. But when the data is fetched and the state is updated, there is no overlay of suggestions! I checked the length of _options and it has all of the results but still setState did not forced Autocomplete widget to rebuild it's overlay. Although when I remove the last character of the TextFormField, it immediately shows the overlay. How can I fix this issue?
In the TextField's onChanged method calls the textEditingController.notifyListeners() and ignores the warnings
example
TextField(
onChanged: (text)async{
timer?.cancel();
timer = Timer(const Duration(milliseconds: 700), () async {
await getData(textEditingController).whenComplete((){
setState(() {
// ignore: invalid_use_of_protected_member
//invalid_use_of_visible_for_testing_member
textEditingController.notifyListeners();
});
});
});
},
)

Flutter BLoC is not updating the state or notifying the changes in the screen or widget

I am building a mobile application using Flutter. I am using BLoC for state management. I am using this library, https://pub.dev/packages/flutter_bloc. I app is emitting the event to the Bloc class. But the state is not updated in the screen or widget.
This is my LoginBloc class:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(LoginState.initial()) {
on<LoginEvent>((event, emit) async {
// yield the state here. check the event and then process the event and yield the state based on the result.
if (event is Login) {
var response = await ApiService.post(ApiEndpoints.login, {
'email': event.email,
'password': event.password
});
try {
if (response.statusCode == 200) {
// TODO: provide implementation
var responseJson = jsonDecode(response.body);
MeData meData = MeData.fromJson(responseJson['data']);
} else {
ApiError apiError = Utilities.parseApiError(response.body);
emit(LoginState(event.email, event.password, false, apiError));
}
} catch (e) {
var apiError = ApiError();
apiError.setGenericErrorMessage("Something went wrong!");
emit(LoginState(event.email, event.password, false, apiError));
}
}
});
}
}
This is my login_event.dart file.
abstract class LoginEvent {
const LoginEvent();
}
// fields are the parameters to be passed to the service class in the bloc class.
class Login extends LoginEvent {
final String email;
final String password;
const Login(this.email, this.password);
}
This is my login_state.dart file
class LoginState {
String email = "";
String password = "";
bool isLoading = false;
ApiError error = ApiError();
String genericFormError = "";
LoginState(String emailParam, String passwordParam, bool isLoadingParam, ApiError errorParam) {
email = emailParam;
password = passwordParam;
isLoading = isLoadingParam;
error = errorParam;
genericFormError = error.getGenericErrorMessage();
}
static LoginState initial()
{
return LoginState("", "", false, ApiError());
}
}
This is my login screen or widget
class _LoginPage extends State<LoginPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String? _email;
String? _password;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title)
),
body: BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
},
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
return Center(
child: Form(
key: _formKey,
child: Column(
children: [
GenericFormError(errorMessage: state.genericFormError),
Padding(
padding: const EdgeInsets.all(10),
child: TextFormField(
decoration: InputDecoration(
labelText: "Email",
border: const OutlineInputBorder(),
errorText: state.error.getFieldError("email")
),
onChanged: (value) => setState(() {
_email = value;
}),
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter email";
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.all(10),
child: TextFormField(
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Password",
border: const OutlineInputBorder(),
errorText: state.error.getFieldError("password")
),
onChanged: (value) => setState(() {
_password = value;
}),
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter password";
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
var isValid = _formKey.currentState!.validate();
if (isValid) {
context.read<LoginBloc>().add(Login(_email.toString(), _password.toString()));
}
},
child: const Text('Login'),
),
),
),
],
),
)
);
}
),
),
);
}
}
As you can see when the login button is clicked in the login screen/ widget, it is triggering the Login event.
This line in the LoginBloc was executed.
var apiError = ApiError();
apiError.setGenericErrorMessage("Something went wrong!");
emit(LoginState(event.email, event.password, false, apiError));
But the state is not updated in the login screen or widget.
The bloc listener method is in run as well. How can I fix it?
You are currently not emitting any state if the response code is 200, so nothing would happen even if you weren't getting an error.
You should have an abstract Class of LoginState then define at least 3 classes ie. initial state of NotLoggedIn, LoginSuccess and LoginError that extend LoginState.
Your bloc should emit the corresponding states depending on each potential response and the BlocBuilder should respond to each state accordingly.