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'),
),
Related
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;
}
}
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!!!"),
),
),
],
),
);
}
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
});
} ,
),
),
),
);
}
}
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()
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,
);
}
}