Losing state of each form field when the form is scrolled off screen?

Viewed 1957

How can I can save state of each form field when the form is scrolled off screen? I lose the values I typed in the TextFormFields when I scroll up and the fields are off the screen.

I have heard the suggestion of saving it in controller and then assigning to initialvalue of TextFormField as seen in the code but it doesn't work for me. What am I missing?

TextEditingController _controller = new TextEditingController();

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      key: scaffoldKey,
      appBar: new AppBar(
        title: new Text('Enter Vehicle'),
      ),
      body: new Padding(
          padding: const EdgeInsets.all(16.0),
          child: new Form(
              key: formKey,
              child: new ListView(children: <Widget>[
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Vehicle Number'),
                  controller: _controller,
                  initialValue: _controller.text,
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Tag'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Make'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Model'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Year'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Vehicle Type'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Location'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Fuel'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Other'),
                ),
                new TextFormField(
                  decoration: new InputDecoration(labelText: 'Your password'),
                  validator: (val) =>
                  val.length < 6 ? 'Password too short.' : null,
                  onSaved: (val) => _password = val,
                  obscureText: true,
                ),
                new RaisedButton(
                  onPressed: _submit,
                  child: new Text('Login'),
                ),
              ],)
          )
      ),
    );
  }
5 Answers

There are two solutions for this:

  1. Prevent the objects from recycling
  2. Save the state in the parent widget

So, while a FormField - of which TextFormField derives - is stateful, putting it in a list - of any description - will risk rebuilding it.

Option 1:

Change the List to a Column and wrap it in a List or SingleChildListView, its quick and hacky but might have some performance issues for lots of items

List(
   children: [...]
)

nb. if you have a listview.builder you are gonna need to map it out and not use the builder, or progress to option 2.

to

SingleChildScrollView(
   child: Column(
       children: [...]
   )
)

Option 2:

Create a controller for each field and save it in the parent widget, or save the data to a map and set its initial data. You can work it out but either way you need to save it in the parent widget.

You don't need to use initialValue that way when using a TextFormField. In fact, if _controller.text is null it will give an assertion error.

To have correctly working TextFormFields ensure that:

  • You are using a different TextEditingController on each of the fields.
  • You are disposing your TextEditingController after being done.
  • Your form is inside a StatefulWidget.

Working example:

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  TextEditingController _vehicleNumber;

  @override
  void initState() {
    super.initState();
    _vehicleNumber = new TextEditingController();
  }

  @override
  void dispose() {
    super.dispose();
    _vehicleNumber.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Enter Vehicle'),
      ),
      body: new Padding(
          padding: const EdgeInsets.all(16.0),
          child: new Form(
              child: new ListView(
            children: <Widget>[
              new TextFormField(
                decoration: new InputDecoration(labelText: 'Vehicle Number'),
                controller: _vehicleNumber,
              ),
              // All the other fields here
              // All should have its own controller
            ],
          ))),
    );
  }
}

You need a dedicated TextEditingController for each TextFormField and I don't think you need to have the initialValue attribute on the fields.

Have a look at this answer https://stackoverflow.com/a/45242235/6414732, that may help as well.

  new Container(
                  padding: const EdgeInsets.only(
                      left: 20.0, right: 20.0, top: 0.0, bottom: 0.0),
                  child: new EnsureVisibleWhenFocused(
                    focusNode: _focusNode_nooftickets,
                    child: new TextFormField(
                      controller: _ticketscontroller,
                      initialValue: _ticketscontroller.text,
                      focusNode: _focusNode_nooftickets,
                      keyboardType: TextInputType.number,
                      inputFormatters: <TextInputFormatter>[
                        WhitelistingTextInputFormatter.digitsOnly
                      ],
                      validator: validatenooftickets,
                      onSaved: (String value) {
                        ticketmode.nooftickets = value;
                      },
                    ),
                  ),
                ),

I used a column instead of ListView and wrapped it with a singleChildScrollView and it works

Related