I want to build a form where I have multiple TextField widgets, and want to have a button that composes and e-mail when pressed, by passing the data gathered from these fields.

For this, I started building an InheritedWidget to contain TextField-s, and based on the action passed in the constructor - functionality not yet included in the code below - it would return a different text from via toString method override.

As I understood, an InheritedWidget holds it's value as long as it is part of the current Widget tree (so, for example, if I navigate from the form it gets destroyed and the value is lost).

Here is how I built my TextForm using InheritedWidget:

class TextInheritedWidget extends InheritedWidget {
  const TextInheritedWidget({
    Key key,
    this.text,
    Widget child}) : super(key: key, child: child);

  final String text;

  @override
  bool updateShouldNotify(TextInheritedWidget old) {
    return text != old.text;
  }

  static TextInheritedWidget of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(TextInheritedWidget);
  }

}

class TextInputWidget extends StatefulWidget {
  @override
  createState() => new TextInputWidgetState();
}

class TextInputWidgetState extends State<TextInputWidget> {
  String text;
  TextEditingController textInputController = new TextEditingController();

  @override
  Widget build(BuildContext context) {
    return new TextInheritedWidget(
      text: text,
      child: new TextField(
        controller: textInputController,
        decoration: new InputDecoration(
          hintText: adoptionHintText
        ),
        onChanged: (text) {
          setState(() {
            this.text = textInputController.text;
          });
        },
      ),
    );
  }

  @override
  String toString({DiagnosticLevel minLevel: DiagnosticLevel.debug}) {
    // TODO: implement toString
    return 'Név: ' + text;
  }
}

And here is the button that launches the e-mail sending:

TextInputWidget nameInputWidget = new TextInputWidget();
  TextInheritedWidget inherited = new TextInheritedWidget();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Örökbefogadás'),
      ),
      body: new Container(
        padding: const EdgeInsets.all(5.0),
        child: new ListView(
          children: <Widget>[
            new Text('Név:', style: infoText16BlackBold,),
            nameInputWidget,
            new FlatButton(onPressed: () {
              launchAdoptionEmail(nameInputWidget.toString(), 'kutya');
            },
                child: new Text('Jelentkezem'))

          ],
        ),
      ),

    );
  }

My problem is that the nameInputWidget.toString() simply returns TextInputWidget (class name) and I can't seem to find a way to access the TextInputWidgetState.toString() method.

I know that TextInheritedWidget holds the text value properly, but I'm not sure how I could access that via my nameInputWidget object.

Shouldn't the TextInputWidget be able to access the data via the context the InheritedWidget uses to determine which Widget to update and store the value of?


Solution 1: Rémi Rousselet

This is not possible. Only children of an InheritedWidget can access it's properties

The solution would be to have your InheritedWidget somewhere above your Button. But that imply you'd have to refactor to take this into account.


Solution 2: Zoltán Györkei

Following Rémi's remarks, I came up with a working solution, albeit I'm pretty sure it is not the best and not to be followed on a massive scale, but should work fine for a couple of fields.

The solution comes by handling all TextField widgets inside one single State, alongside the e-mail composition.

In order to achieve a relatively clean code, we can use a custom function that build an input field with the appropriate data label, which accepts two input parameters: a String and a TextEditingController.

The label is also used to determine which variable the setState() method will pass the newly submitted text.

Widget buildTextInputRow(var label, TextEditingController textEditingController) {
    return new ListView(
      shrinkWrap: true,
      children: <Widget>[
        new Row(
          children: <Widget>[
            new Expanded(
              child: new Container(
                  padding: const EdgeInsets.only(left: 5.0, top: 2.0, right: 5.0  ),
                  child: new Text(label, style: infoText16BlackBold)),
            ),
          ],
        ),
        new Row(
          children: <Widget>[
            new Expanded(
              child: new Container(
                  padding: const EdgeInsets.only(left: 5.0, right: 5.0),
                  child: new TextField(
                      controller: textEditingController,
                      decoration: new InputDecoration(hintText: adoptionHintText),
                      onChanged: (String str) {
                        setState(() {
                          switch(label) {
                            case 'Név':
                              tempName = 'Név: ' + textEditingController.text + '\r\n';
                              break;
                            case 'Kor':
                              tempAge = 'Kor: ' + textEditingController.text + '\r\n';
                              break;
                            case 'Cím':
                              tempAddress = 'Cím: ' + textEditingController.text + '\r\n';
                              break;
                            default:
                              break;
                          }
                        });
                      }
                  )),
            ),
          ],
        )
      ],
    );
  }

The problem is obviously that you will need a new TextEditingController and a new String to store every new input you want the user to enter:

  TextEditingController nameInputController = new TextEditingController();
  var tempName;
  TextEditingController ageInputController = new TextEditingController();
  var tempAge;
  TextEditingController addressInputController = new TextEditingController();
  var tempAddress;

This will result in a lot of extra lines if you have a lot of fields, and you will also have to update the composeEmail() method accordingly, and the more fields you have, you will be more likely to forget a couple.

  var emailBody;
  composeEmail(){
    emailBody = tempName + tempAge + tempAddress;
    return emailBody;
  }

Finally, it is time to build the form:

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Örökbefogadás'),
      ),
      body: new ListView(
        children: <Widget>[
          buildTextInputRow('Név', nameInputController),
          buildTextInputRow('Kor', ageInputController),
          buildTextInputRow('Cím', addressInputController),
          new FlatButton(onPressed: () { print(composeEmail()); }, child: new Text('test'))
        ],
      ),
    );
  }

For convenience, I just printed the e-mail body to the console while testing

I/flutter ( 9637): Név: Zoli
I/flutter ( 9637): Kor: 28
I/flutter ( 9637): Cím: Budapest

All this is handled in a single State.