Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter GlobalKey current state is null

Tags:

state

flutter

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.

like image 340
Amine Avatar asked Jul 25 '19 19:07

Amine


3 Answers

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.");
like image 70
JJ_Coder4Hire Avatar answered Oct 18 '22 23:10

JJ_Coder4Hire


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).

like image 20
Amine Avatar answered Oct 18 '22 22:10

Amine


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;
        });
like image 2
Alejandro Serret Avatar answered Oct 18 '22 23:10

Alejandro Serret