Flutter Form Validation with nested Widgets - flutter

So for my Registration Screen, I have two TextFormField the user has to fill out: email and password. Because I use the same Styling etc I wanted to refactor the TextFormField into a seperate Widget. In the Main Widget, when the user presses the Register Button, I want to validate all three Fields. I have tried it with a GlobalKey, but I get the Error message "Multiple widgets used the same GlobalKey."
Here is my Registration Screen code:
class RegistrationScreen extends StatefulWidget {
#override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
String email;
String password;
final formKey = GlobalKey<FormState>();
void handleRegisterEmail(){
if (formKey.currentState.validate()){
print('Handling Register');
}
}
void handleEmailChanged(String value) {
setState(() {
email = value;
});
}
void handlePasswordChanged(String value) {
setState(() {
password = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
AuthTextField(
key: formKey,
type: 'Email',
onChanged: handleEmailChanged,
),
AuthTextField(
key: formKey
type: 'Password',
onChanged: handlePasswordChanged,
),
AuthButton(
text: 'Register',
onPressed: handleRegisterEmail,
),
]
),
);
}
}
And here is the code for the AuthTextField:
class AuthTextField extends StatefulWidget {
final String emailOrPassword;
final Function onChanged;
final GlobalKey<FormState> key; //Here passing the key
AuthTextField({this.onChanged, this.emailOrPassword, this.key})
: super(key: key);
#override
_AuthTextFieldState createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<AuthTextField> {
String validateForm(String value) { //two different validators
if (widget.emailOrPassword == 'Email') {
validateFormEmail(value);
} else {
validateFormPassword(value);
}
}
String validateFormEmail(String value) {
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String validateFormPassword(String value) {
if (value.length < 6) {
return 'Password must be at least 6 characters.';
}
return null;
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: TextFormField(
key: widget.key, //Passing the key
validator: validateForm,
keyboardType: (widget.emailOrPassword == 'Email')
? TextInputType.emailAddress
: TextInputType.text,
onChanged: widget.onChanged,
//plus some more styling
),
),
);
}
}
And as I said I get and error message. Any suggestion how to solve this? I tried a bunch of different things (e.g. Wrapping the column in RegistrationScreen in a Form and passing the key only to that form) but nothing work.

The issue here is that you are setting the key of the AuthTextFields instead of storing it in as a separate field on the object. This is setting it as the key for both the AuthTextField and the Form object.
This is what you would want to do instead:
class AuthTextField extends StatefulWidget {
final String emailOrPassword;
final Function onChanged;
final GlobalKey<FormState> formKey; //Here passing the key
AuthTextField({Key key, this.onChanged, this.emailOrPassword, this.formKey})
: super(key: key);
#override
_AuthTextFieldState createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<AuthTextField> {
String validateForm(String value) { //two different validators
if (widget.emailOrPassword == 'Email') {
validateFormEmail(value);
} else {
validateFormPassword(value);
}
}
String validateFormEmail(String value) {
if (!RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String validateFormPassword(String value) {
if (value.length < 6) {
return 'Password must be at least 6 characters.';
}
return null;
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: TextFormField(
//key: widget.key, //Don't pass the key to the TextFormField only the form
validator: validateForm,
keyboardType: (widget.emailOrPassword == 'Email')
? TextInputType.emailAddress
: TextInputType.text,
onChanged: widget.onChanged,
//plus some more styling
),
),
);
}
}
Then you need wrap your widgets in a Form widget:
class RegistrationScreen extends StatefulWidget {
#override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
String email;
String password;
final formKey = GlobalKey<FormState>();
void handleRegisterEmail(){
if (formKey.currentState.validate()){
print('Handling Register');
}
}
void handleEmailChanged(String value) {
setState(() {
email = value;
});
}
void handlePasswordChanged(String value) {
setState(() {
password = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: formKey, //Wrap your column in this form widget and set the key
child: Column(
children: <Widget>[
AuthTextField(
key: formKey,
type: 'Email',
onChanged: handleEmailChanged,
),
AuthTextField(
key: formKey
type: 'Password',
onChanged: handlePasswordChanged,
),
AuthButton(
text: 'Register',
onPressed: handleRegisterEmail,
),
]
),
),
);
}
}

Related

Flutter - form validate and focus problem

I am trying to do website form validation and focus on the first error field for users automatically. I found a helpful solution to get to the first error field from [here][1].
However, the focus manager solution does not work properly as I expected when there are dynamic fields or conditional fields wrapped with if...[]. When the form validates, the focus goes to the last field and skipped both dynamic and conditional fields.
I solve the conditional field problem by declaring each field in sequence before the return statement, but the dynamic fields still encounter the same problem on validation. Can anyone tell me what is happening within Widget build and is there any possible solutions to this problem? The following is the code I tested.
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _formKey = GlobalKey<FormState>();
final _formValidationManager = FormValidationManager();
int count = 3;
bool isChecked = false;
#override
Widget build(BuildContext context) {
Widget widget1 = Column(
children: [
Text('field 1'),
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
focusNode: _formValidationManager.getFocusNodeForField('field1'),
validator: _formValidationManager.wrapValidator('field1', (value) {
if (value!.isEmpty) {
return 'Please enter a value';
}
return null;
}))
],
);
Widget widget2 = Column(
children: [
Text('field 2'),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field2'),
validator: _formValidationManager.wrapValidator('field2', (value) {
if (value!.isEmpty) {
return 'Please enter a value';
}
return null;
}))
],
);
Widget dynamicField = ListView.separated(
itemCount: count,
separatorBuilder: (BuildContext context, int index) => Divider(height: 1),
itemBuilder: (BuildContext context, int index) {
return DynamicTextField(
formValidationManager: _formValidationManager, index: index);
},
shrinkWrap: true,
physics: const ScrollPhysics(),
);
Widget widget3 = Row(
children: [
Checkbox(
value: isChecked,
onChanged: (value) {
isChecked = !isChecked;
setState(() {});
}),
SizedBox(
width: 16,
),
Text("Open conditional Field"),
],
);
Widget conditionalWidget = Column(
children: [
Text('conditional field'),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field4'),
validator: _formValidationManager.wrapValidator('field4', (value) {
if (value!.isEmpty) {
return 'Please enter a value';
}
return null;
}))
],
);
Widget widget4 = Column(
children: [
Text("field 3"),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field5'),
validator: _formValidationManager.wrapValidator('field5', (value) {
if (value!.isEmpty) {
return 'Please enter a value';
}
return null;
}))
],
);
return Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: [
widget1,
widget2,
widget3,
if (isChecked) ...[conditionalWidget],
dynamicField,
widget4,
ElevatedButton(
onPressed: () {
if (!_formKey.currentState!.validate()) {
_formValidationManager.erroredFields.first.focusNode
.requestFocus();
}
},
child: Text('SUBMIT'))
],
),
),
);
}
#override
void dispose() {
_formValidationManager.dispose();
super.dispose();
}
}
class FormValidationManager {
final _fieldStates = Map<String, FormFieldValidationState>();
FocusNode getFocusNodeForField(key) {
_ensureExists(key);
return _fieldStates[key]!.focusNode;
}
FormFieldValidator<T> wrapValidator<T>(
String key, FormFieldValidator<T> validator) {
_ensureExists(key);
return (input) {
final result = validator(input);
_fieldStates[key]!.hasError = (result?.isNotEmpty ?? false);
return result;
};
}
List<FormFieldValidationState> get erroredFields => _fieldStates.entries
.where((s) => s.value.hasError)
.map((s) => s.value)
.toList();
void _ensureExists(String key) {
_fieldStates[key] ??= FormFieldValidationState(key: key);
}
void dispose() {
_fieldStates.entries.forEach((s) {
s.value.focusNode.dispose();
});
}
}
class FormFieldValidationState {
final String key;
bool hasError;
FocusNode focusNode;
FormFieldValidationState({required this.key})
: hasError = false,
focusNode = FocusNode();
}
class DynamicTextField extends StatelessWidget {
final FormValidationManager formValidationManager;
final int index;
const DynamicTextField(
{Key? key, required this.formValidationManager, required this.index})
: super(key: key);
#override
Widget build(BuildContext context) {
return TextFormField(
focusNode: formValidationManager.getFocusNodeForField('node$index'),
validator: formValidationManager.wrapValidator('node$index', (value) {
if (value!.isEmpty) {
return 'Please enter a value';
}
return null;
}));
}
}```
Thank you very much!
[1]: https://stackoverflow.com/questions/63833619/flutter-forms-get-the-list-of-fields-in-error

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

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

Flutter form with provider

I'm new with Flutter and provider.
I'm trying to make a form with provider in order to separate my logic in my code but I'm struggling ...
My form in the screen :
class CalculatorScreen extends StatefulWidget{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen> {
final TextEditingController _controllerDistance = TextEditingController();
#override
void dispose(){
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context)
{
var _formCalculatorProvider = Provider.of<FormCalculatorNotifier>(context);
return SingleChildScrollView(
child: Column(
children: [
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formCalculatorProvider.globalFormKey,
autovalidate: _formCalculatorProvider.autovalidate,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
),
controller: _controllerDistance,
keyboardType : TextInputType.number,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formCalculatorProvider.saveDistance(value);
}
),
],
),
),
ButtonComponent.primary(
context: context,
text: "Send",
onPressed: _formCalculatorProvider.submit,
),
],
)
],
),
);
}
}
And my notifier :
enum FormCalculatorState{
READY,
SUCCESS,
ERROR
}
class FormCalculatorNotifier with ChangeNotifier {
final GlobalKey<FormState> globalFormKey = GlobalKey<FormState>();
FormCalculatorState formState = FormCalculatorState.READY;
bool autovalidate = false;
FormCalculatorModel formData = FormCalculatorModel();
void saveDistance(String value){
print("save");
formData.distance = num.tryParse(value).round();
notifyListeners();
}
void submit(){
if (!globalFormKey.currentState.validate()) {
print("submit");
print(formData);
autovalidate = true;
formState = FormCalculatorState.ERROR;
return;
}
else{
globalFormKey.currentState.save();
}
notifyListeners();
}
Future showErrorNotification(){
// Here I need to know the context
return InfoBarComponent.error(title: AppTextInfobar.ERROR_TITLE, description: AppTextInfobar.ERROR_DESCRIPTION, context: context);
}
How to use my showErrorNotification because I need the context to show my notificationBar ? When I try to add context in the scrren on the submit function I have an error.
Is this the right method?
Did not go through your entire code. But I immediately noticed that notifyListeners is missing in FormCalculatorNotifier class.

Flutter Forms: Get the list of fields in error

Can we get the list of fields in error from Flutter forms after validation ? This will help developers use focus-nodes to redirect the attention to the field in error.
I don't think it is possible to get this kind of information from a Form object or a FormState.
But here is a way around to obtain the result you want (focus on the field in error) :
class _MyWidgetState extends State<MyWidget> {
FocusNode _fieldToFocus;
List<FocusNode> _focusNodes;
final _formKey = GlobalKey<FormState>();
final _numberOfFields = 3;
String _emptyFieldValidator(String val, FocusNode focusNode) {
if (val.isEmpty) {
_fieldToFocus ??= focusNode;
return 'This field cannot be empty';
}
return null;
}
#override
void initState() {
super.initState();
_focusNodes =
List<FocusNode>.generate(_numberOfFields, (index) => FocusNode());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(actions: [
IconButton(
icon: Icon(Icons.check),
onPressed: () {
if (_formKey.currentState.validate()) {
print('Valid form');
} else {
_fieldToFocus?.requestFocus();
_fieldToFocus = null;
}
},
),
]),
body: Form(
key: _formKey,
child: Column(children: [
...List<TextFormField>.generate(
_numberOfFields,
(index) => TextFormField(
decoration: InputDecoration(hintText: "Field $index"),
focusNode: _focusNodes[index],
validator: (val) => _emptyFieldValidator(val, _focusNodes[index]),
),
),
]),
),
);
}
}
You simply need to create a FocusNode for each one of your fields, thanks to that you will be abla to call requestFocus on a precise field (in your case a field considered as invalid). Then in the validator property of your form field, as it is the method called by the FormState.validate(), you need to set a temporary variable which will contains the right FocusNode. In my example I only set the variable _fieldToFocus if it was not already assigned using the ??= operator. After requesting the focus on the node I set _fieldToFocus back to null so it will still works for another validation.
You can try the full test code I have used on DartPad.
Sorry if I have derived a bit from your question but I still hope this will help you.
Expanding on Guillaume's answer, I've wrapped the functionality into a reusable class.
You can view a working example on DartPad here: https://www.dartpad.dev/61c4ccddbf29a343c971ee75e60d1038
import 'package:flutter/material.dart';
class FormValidationManager {
final _fieldStates = Map<String, FormFieldValidationState>();
FocusNode getFocusNodeForField(key) {
_ensureExists(key);
return _fieldStates[key].focusNode;
}
FormFieldValidator<T> wrapValidator<T>(String key, FormFieldValidator<T> validator) {
_ensureExists(key);
return (input) {
final result = validator(input);
_fieldStates[key].hasError = (result?.isNotEmpty ?? false);
return result;
};
}
List<FormFieldValidationState> get erroredFields =>
_fieldStates.entries.where((s) => s.value.hasError).map((s) => s.value).toList();
void _ensureExists(String key) {
_fieldStates[key] ??= FormFieldValidationState(key: key);
}
void dispose() {
_fieldStates.entries.forEach((s) {
s.value.focusNode.dispose();
});
}
}
class FormFieldValidationState {
final String key;
bool hasError;
FocusNode focusNode;
FormFieldValidationState({#required this.key})
: hasError = false,
focusNode = FocusNode();
}
To use it, create your forms as usual, but add a FormValidationManager to your state class, and then use that instance to wrap your validation methods.
Usage:
class _MyWidgetState extends State<MyWidget> {
final _formKey = GlobalKey<FormState>();
final _formValidationManager = FormValidationManager();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field1'),
validator: _formValidationManager.wrapValidator('field1', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field2'),
validator: _formValidationManager.wrapValidator('field2', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
ElevatedButton(
onPressed: () {
if (!_formKey.currentState.validate()) {
_formValidationManager.erroredFields.first.focusNode.requestFocus();
}
},
child: Text('SUBMIT'))
],
),
);
}
#override
void dispose() {
_formValidationManager.dispose();
super.dispose();
}
}

Flutter: objects inside InheritedWidget cannot change value

I have a question about InheritedWidget. Since most of the pages in my apps used the user object, so I created an InheritedWidget class called UserProvider so I don't need to pass the user object along my widget tree. It works fine until I tried to logout and login with another user. The User remains the old one. I do a bit of research and it seems that the value inside InheritedWidget class cannot be changed. It there a way to rewrite it so I can take advantage of InheritedWidget and still able to change the value of the user object?
UserProvider Class:
class UserProvider extends InheritedWidget {
UserProvider({Key key, Widget child, this.user}) : super(key: key, child: child);
final User user;
/* #override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
*/
#override
bool updateShouldNotify(UserProvider oldWidget) {
return user != oldWidget.user;
}
static UserProvider of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(UserProvider) as UserProvider);
}
}
HomePage class:
class HomePage extends StatefulWidget {
HomePage({this.auth, this.onSignedOut,this.userId});
final BaseAuth auth;
final VoidCallback onSignedOut;
final String userId;
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _userName;
String _userEmail;
String _userPicURL;
User currentUser;
void _signOut() async {
try {
await widget.auth.signOut();
widget.onSignedOut();
} catch (e) {
print(e);
}
}
#override
void initState() {
super.initState();
currentUser = User(widget.userId);
currentUser.loadUserData();
...
#override
Widget build(BuildContext context) {
return UserProvider(
user: currentUser,
...
LoginPage class:
class LoginPage extends StatefulWidget {
LoginPage({this.auth, this.onSignedIn});
final BaseAuth auth;
final VoidCallback onSignedIn;
#override
//_LoginPageState createState() => _LoginPageState();
State<StatefulWidget> createState() => _LoginPageState();
}
enum FormType {
login,
register
}
class _LoginPageState extends State<LoginPage> {
final formKey = new GlobalKey<FormState>();
String _uid;
String _email;
String _password;
String _birthday;
String _fullname;
FormType _formType = FormType.login;
bool validateAndSave() {
final form = formKey.currentState;
if (form.validate()) {
form.save();
return true;
} else {
return false;
}
}
void _addData(String email, String fullname, String birthday) async {
_uid = await widget.auth.currentUser();
Firestore.instance.runTransaction((Transaction transaction) async{
Firestore.instance.collection("Users").document(_uid).setData(
{
"id": _uid,
"email" : email,
"fullname": fullname,
"birthday" : birthday
});
});
}
void validateAndSubmit() async{
final form = formKey.currentState;
if (validateAndSave()) {
try {
if (_formType == FormType.login) {
String userId = await widget.auth.signInWithEmailAndPassword( _email.trim(), _password.trim());
} else {
String userId = await widget.auth.createUserWithEmailAndPassword( _email.trim(), _password.trim());
_addData(_email, _fullname, _birthday);
}
widget.onSignedIn();
}
catch (e)
{
print('Error: $e');
}
} else {
print('form is invalid');
}
}
void moveToRegister () {
formKey.currentState.reset();
setState(() {
_formType = FormType.register;
});
}
void moveToLogin () {
formKey.currentState.reset();
setState(() {
_formType = FormType.login;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Login"),
backgroundColor: const Color(0xFF86d2dd),
),
body: new Container(
padding: EdgeInsets.all(16.0),
child: new Form(
key: formKey,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: buildInputs() + buildSubmitButtons(),
)
)
)
);
}
List<Widget> buildInputs() {
if (_formType == FormType.login) {
return [
new TextFormField(
decoration: new InputDecoration(labelText: "Email"),
validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,
onSaved: (value) => _email = value,
),
new TextFormField(
decoration: new InputDecoration(labelText: "Password"),
obscureText: true,
validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,
onSaved: (value) => _password = value,
),
];
} else {
return [
new TextFormField(
decoration: new InputDecoration(labelText: "Email"),
validator: (value) => value.isEmpty ? 'Email can\'t be empty' : null,
onSaved: (value) => _email = value,
),
new TextFormField(
decoration: new InputDecoration(labelText: "Password"),
obscureText: true,
validator: (value) => value.isEmpty ? 'Password can\'t be empty' : null,
onSaved: (value) => _password = value,
),
new TextFormField(
decoration: new InputDecoration(labelText: "Name "),
validator: (value) => value.isEmpty ? 'Name can\'t be empty' : null,
onSaved: (value) => _fullname = value,
),
new TextFormField(
decoration: new InputDecoration(labelText: "Birthday (MM/DD)"),
validator: (value) => value.isEmpty ? 'Birthday can\'t be empty' : null,
onSaved: (value) => _birthday = value,
),
];
}
}
List<Widget> buildSubmitButtons() {
if (_formType == FormType.login) {
return [
new RaisedButton(
child: new Text('Login', style: new TextStyle(fontSize: 20.0)),
onPressed: validateAndSubmit,
),
new FlatButton(
child: new Text('Create an account', style: new TextStyle(fontSize: 20.0)),
onPressed: moveToRegister,
)
];
} else {
return [
new RaisedButton(
child: new Text('Create an account', style: new TextStyle(fontSize: 20.0)),
onPressed: validateAndSubmit,
),
new FlatButton(
child: new Text('Have an account? Login', style: new TextStyle(fontSize: 20.0)),
onPressed: moveToLogin,
)
];
}
}
}
I'm experimenting with InheritedWidget myself. After reading https://stackoverflow.com/a/51912243/7050833 I would try placing the UserProvider above the MaterialApp.
UserProvider(child: MaterialApp(...