What I'm basically trying to do (And I did, details up ahead) is create a screen with several forms the content of which is changed using a button (NEXT). So I have 3 forms, when I press next, I save the first form and pass to the other, what I did so far works when I put my class in main.dart. For example, here is my main.dart content.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Entries'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
static final _scafoldKey = new GlobalKey<ScaffoldState>();
final firstForm = new FirstForm(title: "First Form");
final secondForm = new SecondForm(title: "First Form",);
final thirdForm = new ThirdForm(title: "First Form");
final Map<int,Widget> forms = {};
final Map<String,Map> allValues = new Map();
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget currentForm;
final Firestore _fireStore = Firestore.instance;
Map<String,Map> thirdFormNested = new Map();
int thirdFormPos = 1;
@override
void initState() {
widget.forms[0] = widget.firstForm;
widget.forms[1] = widget.secondForm;
widget.forms[2] = widget.thirdForm;
currentForm = widget.forms[0];
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: MyHomePage._scafoldKey,
appBar: AppBar(
title: Text(widget.title),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SingleChildScrollView(
child: currentForm,
),
floatingActionButton: Stack(
children: <Widget>[
Align(
alignment: Alignment(0.4, 1),
child: SizedBox(
width: 75,
child: FloatingActionButton(
shape: RoundedRectangleBorder(),
child: Text("NEXT"),
onPressed: () {
GlobalKey<FormState> key;
if(currentForm is FirstForm) {
key= widget.firstForm.formKey;
if(key.currentState.validate()){
key.currentState.save();
widget.allValues["First Form"] = widget.firstForm.values;
setState(() {
currentForm = widget.forms[1];
});
}
}
else if(currentForm is SecondForm) {
setState(() {
currentForm = widget.forms[2];
});
}
else if (currentForm is ThirdForm) {
key = widget.thirdForm.formKey;
if(key.currentState.validate()){
key.currentState.save();
var tmp = widget.thirdForm.values;
widget.thirdForm.values = <String,String>{};
thirdFormNested.addAll({"Bit $thirdFormPos":tmp});
key.currentState.reset();
widget.thirdForm.build(context);
thirdFormPos++;
}
}
},
),
),
),
Align(
alignment: Alignment(1, 1),
child: SizedBox(
width: 75,
child: FloatingActionButton(
shape: RoundedRectangleBorder(),
child: Text("SUBMIT"),
onPressed: () async {
GlobalKey<FormState> key;
if(currentForm is FirstForm) {
key= widget.firstForm.formKey;
if(key.currentState.validate()) {
key.currentState.save();
await _fireStore.collection('form').document(Uuid().v1()).setData(widget.firstForm.values);
}
}else if(currentForm is SecondForm) {
await _fireStore.collection('form').document(Uuid().v1()).setData(widget.firstForm.values);
}else if(currentForm is ThirdForm) {
key= widget.thirdForm.formKey;
if(key.currentState.validate()){
key.currentState.save();
if(thirdFormNested.length == 0) {
var tmp = widget.thirdForm.values;
widget.thirdForm.values = <String,String>{};
thirdFormNested.addAll({"Bit 1":tmp});
}else {
var tmp = widget.thirdForm.values;
widget.thirdForm.values = <String,String>{};
thirdFormNested.addAll({"Bit $thirdFormPos":tmp});
}
widget.allValues["Third Form"] = thirdFormNested;
await _fireStore.collection('form').document(Uuid().v1()).setData(widget.allValues);
}
}
},
),
),
),
],
)
);
}
}
And this is the first form (other forms are the same, the inputs are the only difference):
class FirstForm extends StatelessWidget {
FirstForm({Key key, this.title}) : super(key: key);
final String title;
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
Map<String,String> _values = new Map();
get formKey => _formKey;
get values => _values;
set values(v) => _values = v;
@override
Widget build(BuildContext context) {
print("FORM KEY: ${_formKey.currentState}");
_values['Spud Date'] = _formatDate(DateTime.now().toString());
_values['TD Date'] = _formatDate(DateTime.now().toString());
return Form(
key: _formKey,
child: Column(
children: <Widget>[
ListTile(
leading: Text(
"API#",
style: TextStyle(
fontSize: 18,
),
),
title: TextFormField(
decoration: InputDecoration(
hintText: "<api>"
),
validator: (value) {
if(value.isEmpty) {
return "Please enter API value!";
}
return null;
},
onSaved: (value) {
_values['API#'] = value;
},
),
..... Other inputs
Padding(
padding: EdgeInsets.symmetric(vertical: 36.0),
)
],
),
);
}
String _formatDate(String date) {
DateTime d = DateTime.parse(date);
return DateFormat("dd-MM-yyyy").format(d);
}
}
So each form has a formkey that I pass to the Form(key: ..). I instantiate each form in the MyHomePage class and I use widget.firstForm.formKey
to retrieve the form key to use it for validation.
So far this has been working perfectly. However, when I tried to fit my work into an existing app. It doesn't anymore.
In the existing app, I have a drawer. An item of the drawer is called Forms, which takes me to the "MyHomePage" that I renamed to Forms now. So the code is still the same, the only thing I removed is the runApp() and MyApp class which are in a different file now. This is the drawer item code:
ListTile(
title: new Text('Forms'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
maintainState: true,
builder: (context) => Forms(title: "Forms")
),
);
},
),
Now, using this appraoch. When clicking on Next in the Forms screen, I get the error:
validate() called on null
So after a few hours, I figured out that it's actually the GlobalKey of my forms that returns a null state. The question is why? And how can I solve this.
TL;DR: I have 3 forms with their respective GlobalKeys. The keys are used to validate the forms in another widget, which wraps these forms. The way I approached their creation works when I have the wrapper widget in the default MyHomePage class in main.dart. However, when moving the same wrapper class to an external file and renaming the class to a more appropriate name, the GlobalKeys states are now null.
EDIT: One extra awkward behavior I noticed, is that when I fill the form (FirstForm) and press Next for the first time when I run the app, I get the null error. However, on the first run of the app, if I press Next without filing the form, the validator works and it shows me the errors for filling the inputs.