Home > Software design >  Flutter Unhandled Exception: This widget has been unmounted, so the State no longer has a context
Flutter Unhandled Exception: This widget has been unmounted, so the State no longer has a context

Time:09-07

I keep getting this error when trying to post data and the solutions say to check if(mounted) before calling setState but I don't know where this setState is? The code is below; the function is in the first widget and then I call it in another widget:

// ignore_for_file: use_build_context_synchronously

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:mne/Forms/form_model.dart';

import 'package:shared_preferences/shared_preferences.dart';

import '../Network/api.dart';

class FormWidget extends StatefulWidget {
  const FormWidget({Key? key}) : super(key: key);

  @override
  State<FormWidget> createState() => FormWidgetState();
}

class FormWidgetState extends State<FormWidget> {
  // to show error message

  _showScaffold(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text(message, style: const TextStyle(color: Colors.black)),
        duration: const Duration(seconds: 20),
        action: SnackBarAction(
          textColor: Colors.white,
          label: 'Press to go back',
          onPressed: () {
            Navigator.of(context).pop();
          },
        ),
        backgroundColor: Colors.redAccent));
  }

  final List<FormModel> _formfields = [];
  var loading = false;
  var isEmpty = false;
  List activityResponses = [];
  late Map<String, List<dynamic>> data;

// to fetch form fields
  fetchFormFields() async {
    setState(() {
      loading = true;
    });

    WidgetsFlutterBinding.ensureInitialized();
    SharedPreferences localStorage = await SharedPreferences.getInstance();

    var saved = localStorage.getString('activitytask_id');

    var res = await Network().getData('mobile/activity-fields/$saved');

    if (res.statusCode == 200) {
      final data = jsonDecode(res.body);
      final tdata = data['data'];
      if (tdata.length == 0) {
        Navigator.of(context).pushReplacementNamed('error');
      }

      var formfieldsJson = tdata;

      setState(() {
        for (Map formfieldJson in formfieldsJson) {
          _formfields.add(FormModel.fromJson(formfieldJson));
        }
        loading = false;
      });
    }
  }

  final TextEditingController _textController = TextEditingController();
  final TextEditingController _numberController2 = TextEditingController();

  @override
  void initState() {
    super.initState();
    fetchFormFields();
  }

  submitResult() async {
    WidgetsFlutterBinding.ensureInitialized();
    SharedPreferences localStorage = await SharedPreferences.getInstance();
    final String? responseData = localStorage.getString('responses');
    final String responseList = json.decode(responseData!);
    final List responseList2 = [responseList];

    debugPrint(responseList.toString());
    data = {"activity_responses": responseList2};
    var res = await Network().authData(data, 'mobile/activity-result');

    if (res.statusCode == 200) {
      Navigator.of(context).pushReplacementNamed('activitytasks');
    } else {
      Navigator.of(context).pushReplacementNamed('error');
    }
  }

  @override
  void dispose() {
    _textController.dispose();
    _numberController2.dispose();

    super.dispose();
  }

  // to display form fields
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
            color: Colors.white,
            child: loading
                ? const Center(child: CircularProgressIndicator())
                : ListView.builder(
                    itemCount: _formfields.length,
                    itemBuilder: (context, i) {
                      final nDataList = _formfields[i];

                      return Form(
                          child: Column(children: [
                        Column(children: [
                          if (nDataList.type == 'text')
                            Column(children: [
                              Container(
                                alignment: Alignment.centerLeft,
                                padding:
                                    const EdgeInsets.only(bottom: 5, left: 6),
                                child: Text('Add a ${nDataList.name}',
                                    style: const TextStyle(
                                        fontSize: 16,
                                        fontWeight: FontWeight.bold)),
                              ),
                              Container(
                                  margin: const EdgeInsets.all(8),
                                  child: TextFormField(
                                    controller: _textController,
                                    decoration: InputDecoration(
                                        contentPadding:
                                            const EdgeInsets.all(15),
                                        border: const OutlineInputBorder(),
                                        filled: true,
                                        fillColor: Colors.grey[200],
                                        labelText: nDataList.name),
                                  )),
                              Container(
                                alignment: Alignment.centerRight,
                                child: ElevatedButton(
                                  style: ButtonStyle(
                                      backgroundColor:
                                          MaterialStateProperty.all<Color>(
                                              Colors.green)),
                                  onPressed: () async {
                                    var responseData = {
                                      "activity_field_id": nDataList.id,
                                      "value": _textController.text
                                    };
                                    activityResponses.add(responseData);
                                    debugPrint(activityResponses.toString());
                                  },
                                  child: const Text('Save',
                                      style: TextStyle(color: Colors.white)),
                                ),
                              ),
                            ]),
                          if (nDataList.type == 'number')
                            Column(children: [
                              Container(
                                alignment: Alignment.centerLeft,
                                padding: const EdgeInsets.only(
                                    bottom: 5, left: 6, top: 5),
                                child: Text('Add a ${nDataList.name}',
                                    style: const TextStyle(
                                        fontSize: 16,
                                        fontWeight: FontWeight.bold)),
                              ),
                              Container(
                                  margin: const EdgeInsets.all(8),
                                  child: TextFormField(
                                    controller: _numberController2,
                                    decoration: InputDecoration(
                                        contentPadding:
                                            const EdgeInsets.all(15),
                                        border: const OutlineInputBorder(),
                                        filled: true,
                                        fillColor: Colors.grey[200],
                                        labelText: nDataList.name),
                                  )),
                              Container(
                                alignment: Alignment.centerRight,
                                child: ElevatedButton(
                                  style: ButtonStyle(
                                      backgroundColor:
                                          MaterialStateProperty.all<Color>(
                                              Colors.green)),
                                  onPressed: () async {
                                    // valueResponses.add(_numberController2.text);
                                    // idResponses.add(nDataList.id);
                                    var numberData = {
                                      "activity_field_id": nDataList.id,
                                      "value": _numberController2.text
                                    };
                                    activityResponses.add(numberData);
                                    debugPrint(activityResponses.toString());

                                    SharedPreferences localStorage =
                                        await SharedPreferences.getInstance();

                                    final String encodedData =
                                        (activityResponses).asMap().toString();
                                    final String encodedData2 =
                                        jsonEncode(encodedData);
                                    localStorage.setString(
                                        'responses', encodedData2);
                                  },
                                  child: const Text('Save',
                                      style: TextStyle(color: Colors.white)),
                                ),
                              ),
                            ]),
                        ]),
                      ]));
                    })));
  }
}

The above code is the first widget where I have written submitResult() . The code below is for the page containing the button that will call submitResult() :

import 'package:flutter/material.dart';

import '../Design/custom_shape.dart';
import 'form_widget.dart';

class Workspace extends StatefulWidget {
  const Workspace({Key? key}) : super(key: key);

  @override
  State<Workspace> createState() => WorkspaceState();
}

class WorkspaceState extends State<Workspace> {


  bool isLoading = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      backgroundColor: const Color.fromARGB(255, 236, 246, 219),
      body: Column(
        children: [
          Container(
              alignment: Alignment.center,
              child: Stack(clipBehavior: Clip.none, children: [
                ClipPath(
                    clipper: CustomShape(),
                    child: Container(
                        padding: const EdgeInsets.only(bottom: 128),
                        height: 335,
                        child: Image.asset('assets/images/fields.png',
                            fit: BoxFit.fitWidth))),
                Container(
                  height: 650,
                  width: double.infinity,
                  color: Colors.transparent,
                ),
                Positioned(
                  top: 140,
                  right: 8,
                  height: 600,
                  child: Container(
                      alignment: Alignment.center,
                      width: 360,
                      margin: const EdgeInsets.all(5),
                      padding: const EdgeInsets.all(5),
                      decoration: const BoxDecoration(
                          color: Colors.white,
                          shape: BoxShape.rectangle,
                          borderRadius: BorderRadius.all(Radius.circular(10))),
                      child: Column(
                        children: [
                          Container(
                              height: 450,
                              width: double.infinity,
                              child: const FormWidget()),
                          if (isLoading)
                            Container(
                              alignment: Alignment.bottomCenter,
                              child: ElevatedButton(
                                style: ButtonStyle(
                                    backgroundColor:
                                        MaterialStateProperty.all<Color>(
                                            Colors.green)),
                                onPressed: () async {
                                  setState(() {
                                    isLoading = false;
                                  });
                                  await FormWidgetState().submitResult();
                                  if (!mounted) return;
                                  setState(() {
                                    isLoading = true;
                                  });
                                  // Navigator.of(context).pop();
                                },
                                child: const Text('Submit',
                                    style: TextStyle(color: Colors.white)),
                              ),
                            )
                          else
                            const Center(
                                child: CircularProgressIndicator(
                                    backgroundColor:
                                        Color.fromRGBO(0, 161, 39, 1)))
                        ],
                      )),
                ),
              ])),
        ],
      ),
    );
  }
}

Any help is appreciated thank you

Edit: Adding this image after removing the

// ignore_for_file: use_build_context_synchronously

to show where the error is. screen snip to show the error

CodePudding user response:

The above code is the first widget where I have written submitResult() . The code below is for the page containing the button that will call submitResult()

Something is wrong. You should not call a method of a State object by simply initialising that State object and calling its method inside a different widget altogether as you did somewhere in the second snippet:

await FormWidgetState().submitResult();

State objects are tied to widgets. Flutter continuously discards and recreates widgets but keeps their State object. For Flutter to properly manage a State object, the tied widget must be part of the widget tree.

The IDE shows the above error because the context of that Navigator is not part of any widget tree. There is no widget linked to the State object when submitResult is called.

From the arrangement in the second code snippet, you have a FormWidget inside a Column. In that same Colum, you have the if-block that has a button with the problematic await FormWidgetState().submitResult(); in its onPressed callback.

Column(
  children: [
    Container(
        height: 450,
        width: double.infinity,
        child: const FormWidget()),
    if (isLoading)
      Container(
        alignment: Alignment.bottomCenter,
          child: ElevatedButton(
            // ...
              onPressed: () async {
                // ...
                await FormWidgetState().submitResult();
                if (!mounted) return;
                // ...
              },
            child: const Text('Submit',
            style: TextStyle(color: Colors.white)),
          ),
        )
    else
      const Center(
        child: CircularProgressIndicator(
          backgroundColor: Color.fromRGBO(0, 161, 39, 1)))
  ],
),

If my guess is right, you want to display the FormWidget and have the submit button to call submitResult() from that FormWidget. And that's why you did await FormWidgetState().submitResult(); in the onPressed callback.

Well, the await FormWidgetState().submitResult(); line is not tied to a widget, it is separate. It has no parent per se and it is not part of the widget tree. So, this explains why you get the above error. The FormWidget in the Container above the if-block is very different from the State widget you reinitialized in the problematic await FormWidgetState().submitResult(); line. When rendering the UI, Flutter had auto-initialized and linked a hidden FormWidgetState object for the above FormWidget.

In fact, if (!mounted) return; really has no effect the way we think it should. Because its own containing widget will always mounted inside that method. But then the preceding problematic line's mounted getter has nothing to do with this one's own. (Hope you understand).

Solution

The solution is to merge the upper and lower part of the Column into one widget. So that submitResult call would share the same context as the displayed widget (or rather will have a valid context).

You have 2 options:

  1. Move this entire column to inside FormWidget
  2. Move the definition submitResult out of FormWidget and into the second code snippet's file. Then call submitResult when the FormWidget's form is filled. You will have to find a way to save data externally (maybe using state management or shared preferences) across both widgets.

I expanded the details/steps of Option 1 as follows:

  1. Let FormWidget take the isLoading bool parameter. The parent (second code snippet) will give it its value: FormWidget(isLoading).
  2. Move the Column of the second code snippet to inside the build method of the first. FormWidgetState's build method should return that column. Your good programming skills will guide you on this step as it is a little technical. The upper part of the Column should be the ListView.builder. The Column should of course be inside the Scaffold's body.
  3. The Column's lower part (the if-else block) should use widget.isLoading as bool condition. Remember that this isLoading came from the parent (step 1).


Noticed some other issue with the first code snippet.

Look at the definition of fetchFormFields() method that is called in initState(). You see, that method seems to have a possibility of an error too. You have an if block that does Navigation but does not add a return statement. So if navigation was successfully, the following setState call might be an error (as the FormWidget is no longer mounted).

The comments in the following code will explain. I added the return statement:

fetchFormFields() async {
  setState(() {
    loading = true;
  });

  WidgetsFlutterBinding.ensureInitialized();
  SharedPreferences localStorage = await SharedPreferences.getInstance();

  var saved = localStorage.getString('activitytask_id');
  var res = await Network().getData('mobile/activity-fields/$saved');

  if (res.statusCode == 200) {
    final data = jsonDecode(res.body);
    final tdata = data['data'];
    if (tdata.length == 0) {
      Navigator.of(context).pushReplacementNamed('error');

      // maybe a return statement is supposed to be found here. 
      // adding return statement here prevents the next setState from being called
      return;
    }

    var formfieldsJson = tdata;

    // because if tdata.length was actually zero, the Navigator would have 
    // navigated out and this widget will no longer be mounted.
    // 
    // setState here would then be called on a "no-longer-existing" widget.
    //
    // But the added return statement in the if block above should prevent this.
    setState(() {
      for (Map formfieldJson in formfieldsJson) {
        _formfields.add(FormModel.fromJson(formfieldJson));
      }
      loading = false;
    });
  }
}
  • Related