28

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.

Amine
  • 1,396
  • 4
  • 15
  • 32
  • Same problem here, when I tried everything in main.dart it worked, it doesn't when in another file. Did you solve it? I found this but didn't help either: https://stackoverflow.com/questions/47121411/flutter-retrieving-top-level-state-from-child-returns-null/47142052#47142052 – cdsaenz Jul 26 '19 at 00:43
  • 1
    @cdsaenz No, I haven't found a solution, I've spent the night searching around for the reason why this is happening with no avail. I think my only solution now is writing my entire work using InheritedWidget. – Amine Jul 26 '19 at 11:26
  • I was going to share my code but it's a bit different, I'm using a Pageview with a form in each and a globalkey for each form. When I hit save from one of those pages/forms, at least one of the three states are null.. Very weird, will try a tabview to see if it's a PageView issue. – cdsaenz Jul 26 '19 at 16:59
  • 1
    @cdsaenz Our issues are the same, it's not related to which widget you're using. It's about accessing currentState of a Globalkey outside of its dart file will produce null, and what makes it even weirder, is that if you access it inside main;dart it works, but doesn't in any other file – Amine Jul 26 '19 at 20:23
  • In general it looks perfect, should work but we must be missing something (or it's really a bug). Did you try with Form.of(context) instead of the key? – cdsaenz Jul 27 '19 at 00:01
  • 1
    @cdsaenz Even with Form.of(context) I still get the null error, this time it's actually the FormState which is null – Amine Jul 27 '19 at 02:40
  • 1
    @cdsaenz After almost 2 days of searching. I finally decided to use an InhertiedWidget as an approach. And it works fine now. – Amine Jul 27 '19 at 03:03
  • great to hear that. You gave me food for thought, i'll be investigating InheritedWidget, looks as a good alternative. For now my own quest I've resolved like this: https://pastebin.com/zeR1bpvV – cdsaenz Jul 27 '19 at 16:30

7 Answers7

9

You need to pass the key into the scaffold widget, then the context and state will be set.

class _MyHomePageState extends State<MyHomePage> {
  GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey(); //so we can call snackbar from anywhere

...

Widget build(BuildContext context) {
    return Scaffold(
        key: _scaffoldKey, //set state and context

...

//define method to call
void showInSnackBar(String value) {
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
        content: new Text(value)
    ));
  }

...

//Then you can call it
showInSnackBar("Proximity Alert, please distance yourself from the person or dangerous object close to you! This incident has been logged.");
JJ_Coder4Hire
  • 4,706
  • 1
  • 37
  • 25
  • This doesn't work when an AppBar action calls `_scaffoldKey.currentState` –  Mar 28 '21 at 20:04
3

If anyone having the same issue. I haven't found a solution to this or at least an explanation to why this happens. However, I switched from using my own approach to using an Inherited Widget to transport my form data while keeping the validation inside each separate form (I was validation inside the wrapper class Forms, now I validate each form in its own class).

Amine
  • 1,396
  • 4
  • 15
  • 32
  • 1
    Yes, exactly. This shouldn't be downvoted, quite the opposite. The solution is not to play with keys and similar stuff but to use the proper mechanisms (NotificationListener, InheritedWidget, etc) that the framework provides. – Gábor Jun 14 '20 at 13:06
  • @Gábor `GlobalKey` is a method the framework provides. As of Flutter 2.2 `GlobalKey` is not deprecated. – JE42 Jul 09 '21 at 13:34
  • `GlobalKey` is a stopgap solution that's available (and will certainly remain so in the future) but it goes against the intrinsic way of how state in handled in Flutter. I use it, too, in some places but when there's a better solution to the problem (and in this case there clearly is) I wouldn't use it, let alone recommend it on a site like this. – Gábor Jul 10 '21 at 15:37
  • Agreed. I'm diving into someone else's code base and they're using Global Keys for everything and its a nightmare. They're very rarely actually needed, there are better ways to manage state even without 3rd party libraries. – Loren.A Aug 09 '22 at 23:04
2

Had an issue very similar, I was using a SliverAnimatedList with a GlobalKey<SliverAnimatedListState> the first time that I hit the entire widget tree the widget draw flawless. This was because before the data came I was drawing an empty list and this action initialized the GlobalKey.

Now the second time I got the currentState being null I already had the data but the widget wasn't visible.

I hate solving my issue this way but... I introduce a small delay of 10ms

Future.delayed(
                  Duration(milliseconds: 10), () => _loadItems(state.meals));
            }

I tried with 1ms and works too.

My entire widget code just in case it can help you guys.

return BlocBuilder(
        condition: (previous, current) =>
                       return previous != current
        ,
        bloc: sl.get<DailyactionitemsBloc>(),
        builder: (context, state) {
         var list = SliverAnimatedList(
            itemBuilder: _buildItem,
            initialItemCount: 0,
            key: _listKey,
          );
          if (state is DailyActionFetched) {

              //BUG I have research this thing a lot
              // https://stackoverflow.com/questions/57209096/flutter-globalkey-current-state-is-null
              Future.delayed(
                  Duration(milliseconds: 10), () => _loadItems(state.meals));

          }
          return list;
        });
Alejandro Serret
  • 1,139
  • 15
  • 16
0

You need to find the solution why key becoming null? Does your widget getting destroy? If yes how can you store your widget state or Can you use the global state?

wVV
  • 63
  • 1
  • 10
0

Another solution is

  1. To inject key into the child widget constructor like Child({Key? key}) : super(key: key);
  2. And pass defined as final GlobalKey<ChildState> _key = GlobalKey();into Child widget from Parent widget as body: Center(child: Child(key: _key))
Riddik
  • 2,565
  • 1
  • 11
  • 21
0

The current state is null due to following reasons

  1. there is no widget in the tree that matches this global key,
  2. that widget is not a StatefulWidget, or the associated State object is not a subtype of T.

you use StatelessWidget, that's why it is null.

Rohan Jariwala
  • 1,564
  • 2
  • 7
  • 24
riobull
  • 11
-6

Change this from

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  static final _scafoldKey = new GlobalKey<ScaffoldState>();
}

to

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  static final GlobalKey<ScaffoldState> _scafoldKey = new GlobalKey<ScaffoldState>();
}

It should work.

Yaobin Then
  • 2,662
  • 1
  • 34
  • 54
Vani
  • 71
  • 4
  • 2
    Why would that work? You just added explicit type declaration that is identical to the implicit type. – EzPizza Aug 03 '21 at 10:15