init state for FutureBuilder flutter - 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;
}
}

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

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.

How to make StatefulWidget with global key in Flutter?

I'm new to Flutter, and I think I might just have things set up weird. Most of what's setup so far is from small tutorials. I currently have my main.dart rendering just a userCheck Widget, which will eventually be used to connect with backend and check whether or not I have a valid user token.
class UserCheck extends StatefulWidget {
const UserCheck({Key key}) : super(key: key);
#override
UserCheckState createState() => UserCheckState();
}
class UserCheckState extends State<UserCheck> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
var userExists = false;
#override
Widget build(BuildContext context) {
return userExists
? Home()
: new LoginForm();
}
}
The Home Widget would eventually load information from the backend, given there's a proper user. Right now, userExists obviously defaults to false. This loads the LoginForm just fine. My current goal is when a user logs in, to assume that it's a good login, then reversing userExists to be true, thus opening the Home page. This is the LoginForm setup:
LoginForm({Key key}) : super(key: key);
UserCheckState parent;
#override
LoginFormState createState() => LoginFormState();
}
class LoginFormState extends State<LoginForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
var userData = {};
bool login = true;
String loginText = 'Sign Up';
void updateField(String value, String field) {
setState(() {
userData = {
...userData,
'$field': value,
};
});
}
void sendHome() {
setState(() {
// this.parent.parent.userExists = true;
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(children: <Widget>[
login
? Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
hintText: 'Enter a username', labelText: 'Username'),
validator: (String value) {
if (value == null || value.isEmpty) {
return 'Please enter a username';
}
updateField(value, 'username');
return null;
},
),
TextFormField(
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Enter your password',
labelText: 'Password'),
validator: (String value) {
if (value == null || value.isEmpty) {
return 'Please enter a password';
}
updateField(value, 'password');
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
print('hit here');
if (_formKey.currentState.validate()) {
print('hit here two');
sendHome();
print(userData);
// if API responds with proper token
// API call to login
}
},
child: Text('Submit'),
),
),
],
),
)
: SignupForm(),
ElevatedButton(
onPressed: () {
setState(() {
login = !login;
loginText = loginText == 'Sign Up' ? 'Log In' : 'Sign Up';
});
},
child: Text(loginText),
)
]),
);
}
}```
I don't know if there's some sort of router that would be better and is already implemented as switching Widgets, but I haven't found anything from my own searches.
You don't have to get back to the userCheck widget if the user logs in. Instead, you can go to the Home screen directly by using Navigator to make a transition from the Login page to the Home page. Put the navigator right after the line the user gets a valid token inside the onPressed function of your Submit button:
ElevatedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
// Goes to Home screen.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Home(),
));
}
},
child: Text('Submit'),
),

Unhandled Exception: Failed assertion: line 183 pos 12: 'email != null': is not true

I am using Flutter to make a registration form using Firebase Authentication but this message keeps showing when I try to handle the authentication while trying to show the user the registration error message using the Scaffold.of(context).showSnackBar SnackBar(content: Text(e.message)at Sign-up widget
First I created an auth.dart file
import 'package:firebase_auth/firebase_auth.dart';
class Auth {
final FirebaseAuth _auth = FirebaseAuth.instance;
//Sign up method
Future SignupClass(String email, String password) async {
final UserCredential user = await _auth.createUserWithEmailAndPassword(
email: email, password: password);
}
Then I created this TextField class to modularize my code
class Textfield extends StatelessWidget {
final String hint;
final IconData icon;
final Function savingData;
String _emptyMessage(String str) {
switch (hint) {
case 'Enter your name':
return 'Name is required';
case 'Enter your e-mail':
return 'Email is required';
case 'Enter your Password':
return 'Password is required';
}
}
Textfield(
{#required this.savingData, #required this.hint, #required this.icon});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: TextFormField(
validator: (value) {
if (value.isEmpty) {
return _emptyMessage(hint);
}
return null;
},
onSaved: savingData
}}
and This is the Sig-up widget
class SignupScreen extends StatelessWidget {
static String id = '/SignupScreen';
GlobalKey<FormState> _globalKey = GlobalKey<FormState>();
String _email, _password, _name;
final _auth = Auth();
#override
Widget build(BuildContext context) {
return Scaffold(Textfield(
hint: 'Enter your name',
icon: Icons.person,
savingData: (value) {
_name = value;
},
),
SizedBox(
height: height * .02,
),
Textfield(
hint: 'Enter your e-mail',
icon: Icons.email,
savingData: (value) {
_email =value;
},
),
SizedBox(
height: height * .02,
),
Textfield(
hint: 'Enter your Password',
icon: Icons.lock,
savingData: (value) {
_password = value;
},
),
SizedBox(
height: height * .05,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 120.0),
child: Builder(
builder: (context) => RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0)),
child: Text(
'Register',
style: TextStyle(color: Colors.white),
),
color: Colors.black,
onPressed: () async {
if (_globalKey.currentState.validate()) {
try {
await _auth.SignupClass(_email, _password);
_globalKey.currentState.save();
}on FirebaseException catch (e) {
print(e.toString());
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(e.message),
),
);
}
}
}),
),
),
)
}
and I wrapped my main function with Firebase Initialize
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
You'll need to convert your widget to a StatefulWidget and call setState() on your text fields' onChanged methods. Because, the widget's state doesn't get updated until a build is called, your _email, _password and _name fields will never be updated. This will fix your issue, but there are better ways of doing this.
There are many state management libraries out there that greatly assist in keeping that kind of state outside the UI (Generally, it's better to separate any non-UI state from the UI). Here are a few recommendations:
BloC
Provider
RxDart
There are many tutorials on YouTube that you can check out. A few I recommend are from AndyJulow, ResoCoder, and MarcusNg.

Manually setting Flutter Validation Error

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