Patrick Lumenus
Patrick Lumenus

Reputation: 1722

setState() called during Widget Build

One of the requirements of my current project is a multi-page sign-up form, with each page performing its own independent validation. The way I ended up deciding to implement this is by splitting it up into several smaller forms, which you can pass some functions in as parameters to be executed when the form validation and/or value change runs. Below is an example of one of the "mini-forms" I created to implement this. This one collects information about the user's name.

import "package:flutter/material.dart";

class SignUpNameForm extends StatefulWidget {
  final Function(String) onFirstNameChange;
  final Function(String) onLastNameChange;
  final Function(bool) onFirstNameValidationStatusUpdate;
  final Function(bool) onLastNameValidationStatusUpdate;
  final String firstNameInitialValue;
  final String lastNameInitialValue;

  SignUpNameForm(
      {this.onFirstNameChange,
      this.onLastNameChange,
      this.onFirstNameValidationStatusUpdate,
      this.onLastNameValidationStatusUpdate,
      this.firstNameInitialValue,
      this.lastNameInitialValue});

  @override
  _SignUpNameFormState createState() => _SignUpNameFormState();
}

class _SignUpNameFormState extends State<SignUpNameForm> {
  final _formKey = GlobalKey<FormState>();
  TextEditingController _firstName;
  TextEditingController _lastName;
  bool _editedFirstNameField;
  bool _editedLastNameField;

  @override
  Widget build(BuildContext context) {

    _firstName.text = widget.firstNameInitialValue;
    _lastName.text = widget.lastNameInitialValue;

    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          TextFormField(
            controller: _firstName,
            autovalidate: true,
            decoration: InputDecoration(
              hintText: "First Name",
            ),
            keyboardType: TextInputType.text,
            onChanged: (String value) {
              _editedFirstNameField = true;
              widget.onFirstNameChange(value);
            },
            validator: (String value) {
              String error;

              if (_editedFirstNameField) {
                error = value.isEmpty ? "This field is required" : null;
                bool isValid = error == null;
                widget.onFirstNameValidationStatusUpdate(isValid);
              }

              return error;
            },
          ),
          TextFormField(
            controller: _lastName,
            autovalidate: true,
            decoration: InputDecoration(
              hintText: "Last Name",
            ),
            keyboardType: TextInputType.text,
            onChanged: (String value) {
              _editedLastNameField = true;
              widget.onLastNameChange(value);
            },
            validator: (String value) {
              String error;

              if (_editedLastNameField) {
                error = value.isEmpty ? "This field is required" : null;
                bool isValid = error == null;
                widget.onLastNameValidationStatusUpdate(isValid);
              }

              return error;
            },
          ),
        ],
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    _firstName = new TextEditingController();
    _lastName = new TextEditingController();
    _editedFirstNameField = false;
    _editedLastNameField = false;
  }

  @override
  void dispose() {
    super.dispose();
    _firstName.dispose();
    _lastName.dispose();
  }
}

Then, in my widget that displays the form components, I do something like this.

class _SignUpFormState extends State<SignUpForm> {

  String _firstName;
  bool _firstNameIsValid;

  String _lastName;
  bool _lastNameIsValid;


  int current;
  final maxLength = 4;
  final minLength = 0;

  @override
  Widget build(BuildContext context) {
    // create the widget
    return Container(
      child: _showFormSection(),
    );
  }

  /// Show the appropriate form section
  Widget _showFormSection() {
    Widget form;

    switch (current) {
      case 0:
        form = _showNameFormSection();
        break;
      case 1:
        form = _showEmailForm();
        break;
      case 2:
        form = _showPasswordForm();
        break;
      case 3:
        form = _showDobForm();
        break;
    }

    return form;
  }

  // shows the name section of the form.
  Widget _showNameFormSection() {
    return Container(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            child: SignUpNameForm(
              firstNameInitialValue: _firstName,
              lastNameInitialValue: _lastName,
              onFirstNameChange: (String value) {
                setState(() {
                  _firstName = value;
                });
              },
              onFirstNameValidationStatusUpdate: (bool value) {
                setState(() {
                  _firstNameIsValid = value;
                });
              },
              onLastNameChange: (String value) {
                setState(() {
                  _lastName = value;
                });
              },
              onLastNameValidationStatusUpdate: (bool value) {
                setState(() {
                  _lastNameIsValid = value;
                });
              },
            ),
          ),
          Container(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                FlatButton(
                  child: Text("Next"),
                  onPressed: (_firstNameIsValid && _lastNameIsValid)
                      ? _showNextSection
                      : null,
                ),
              ],
            ),
          )
        ],
      ),
    );
  }


  /// Shows the next section in the form
  void _showNextSection() {
    print("Current Section: " + current.toString());
    setState(() {
      if (current >= maxLength) {
        current = maxLength;
      } else {
        current++;
      }
    });
    print("Next Section: " + current.toString());
  }

  /// Show the previous section of the form
  void _showPreviousSection() {
    print("Current Section:" + current.toString());
    setState(() {
      if (current <= minLength) {
        current = minLength;
      } else {
        current--;
      }
      print("Previous Section: " + current.toString());
    });
  }

  @override
  void initState() {
    super.initState();
    current = 0;
    _firstName = "";
    _firstNameIsValid = false;
    _lastName = "";
    _lastNameIsValid = false;
    // other initializations
  }
}

As you can see here, I pass in functions to extract the values of the user's name, as well as the status of the validation, and use that to determine whether or not I should enable the "next" button in the form handler widget. This is now causing a problem, specifically because the functions I pass into the "mini-form" invokes initState() while the widget is being rebuilt.

How might I go about handling this? Or, is there a better way I can go about implementing this multi-Page form that is cleaner?

Thanks.

Upvotes: 0

Views: 336

Answers (1)

Randal Schwartz
Randal Schwartz

Reputation: 44056

build() should be fast and idempotent. You should not be calling setState() inside a build. Imagine build() is being called 60 times a second (although it won't be thanks to optimizations) and you'll have the proper mindset.

Upvotes: 1

Related Questions