title: Is it possible to position the error message below a TextFormField?
body: <p>I need to fully customize the rendering of input fields and especially the way error messages are rendered. I need them to be displayed below the field without any other modification on the field.</p>
  
  <p>Right now either the field is resized (default) or fixed when I set <code>helperText: ' '</code> as stated in the doc, but then the field height becomes huge to keep space for the error message.</p>
  
  <p>Of course I can add a <code>Text</code> widget below my field for the error (that's what I did actually) but then I can't use form validation as it relies on the built-in error management of the input widget to display error messages... So this would require to re-create my own form validation.</p>
  
  <p>One thing I've been thinking of would be to prevent the form validation to display the error message but I couldn't find a way to do it.</p>
  
  <p>Any idea?</p>
  
  <p>Edit : some code bellow :</p>
  
  <pre class="lang-dart prettyprint-override"><code>class CustomTextFormField extends StatefulWidget {
    final FormFieldSetter&lt;String&gt; onSaved;
    final ValueChanged&lt;String&gt; onFieldSubmitted;
    final TextInputAction textInputAction;
    final String initialValue;
    final bool autofocus;
    final bool obscureText;
    final String label;
    final bool withDivider;
    final FocusNode focusNode;
    final bool required;
  
    CustomTextFormField(
        {Key key,
        this.onSaved,
        this.onFieldSubmitted,
        this.textInputAction,
        this.initialValue,
        this.autofocus = false,
        this.obscureText = false,
        this.label,
        this.withDivider = false,
        this.focusNode,
        this.required})
        : super(key: key);
  
    CustomTextFormFieldState createState() =&gt; CustomTextFormFieldState();
  }
  
  class CustomTextFormFieldState extends State&lt;CustomTextFormField&gt; with CustomFormFieldState {
    bool _isMasked = false;
    String _showObscureIcon;
    FocusNode _focusNode;
    TextEditingController _controller;
    String _errorText;
  
    /// True if this field has any validation errors.
    bool get hasError =&gt; _errorText != null;
  
    @override
    void initState() {
      // Add a listener to the focusNode to detect when we loose focus
      _focusNode = widget.focusNode != null ? widget.focusNode : FocusNode();
      _focusNode.addListener(lostFocusListener);
  
      // Create the TextEditingController for the field with optional initial value
      _controller = TextEditingController(text: widget.initialValue);
      _controller.addListener(() {
        CustomForm.of(context).fieldDidChange(); // CustomForm is basically a simplified and slightly adapted version of the built-in Form
      });
  
      // Set obscure properties
      if (widget.obscureText) {
        _isMasked = true;
        _showObscureIcon = 'assets/img/icon_displaypassword.png';
      }
  
      super.initState();
    }
  
    /// Displays the field label
    Widget _inputLabel() {
      return Container(
        child: Text(widget.label,
            style: hasError ? TextStyle(color: themeErrorColor) : null),
        margin: const EdgeInsets.only(bottom: 8.0),
      );
    }
  
    /// Displays the field
    Widget _inputField() {
      return Expanded(
          child: TextFormField(
        decoration: InputDecoration(
          enabledBorder: themeInputBorder,
          focusedBorder: themeInputBorder,
          contentPadding: EdgeInsets.all(14.0),
        ),
        textInputAction: widget.textInputAction,
        autofocus: widget.autofocus,
        onSaved: widget.onSaved,
        onFieldSubmitted: widget.onFieldSubmitted,
        obscureText: _isMasked,
        focusNode: _focusNode,
        controller: _controller,
      ));
    }
  
    /// Displays the obscured toggle button
    Widget _obscureButton() {
      return IconButton(
        icon: Image.asset(_showObscureIcon),
        onPressed: _toggleObscureText,
      );
    }
  
    /// Displays the decorated field
    Widget _decoratedField() {
      // Row that contains the input field
      List&lt;Widget&gt; rowChildren = &lt;Widget&gt;[_inputField()];
  
      // If the field must be oscured, we add the toggle button to the row
      if (widget.obscureText) {
        rowChildren.add(_obscureButton());
      }
  
      return Container(
          decoration: hasError ? themeInputErrorDecoration : themeInputDecoration, // themeInputErrorDecoration and themeInputDecoration are LinearGradient defined elsewhere
          padding: const EdgeInsets.all(1.0),
          child: Container(
            decoration: BoxDecoration(
                color: Colors.white, borderRadius: BorderRadius.circular(2.0)),
            child: Row(
              children: rowChildren,
            ),
          ));
    }
  
    /// Displays the error message if any
    Widget _errorLabel() {
      return Offstage(
          offstage: !hasError,
          child: Container(
              margin: EdgeInsets.only(top: 10.0),
              child: Text(_errorText == null ? '' : _errorText,
                  style: TextStyle(color: themeErrorColor))));
    }
  
    /// Toggles beetween obscured/clear text and action icon
    void _toggleObscureText() {
      setState(() {
        _isMasked = !_isMasked;
        _showObscureIcon = _isMasked
            ? 'assets/img/icon_displaypassword.png'
            : 'assets/img/icon_dontdisplaypassword.png';
      });
    }
  
    /// Callback called when the focus has changed. Validates the input text
    void lostFocusListener() {
      if (!_focusNode.hasFocus) {
        if (!touched) {
          touched = true;
        }
      }
    }
  
    /// Saves the field
    void save() {
      widget.onSaved(_controller.text);
    }
  
    /// Resets the field to its initial value.
    void reset() {
      setState(() {
        _controller.text = widget.initialValue;
        _errorText = null;
      });
    }
  
    /// Validates the field and set the [_errorText]. Returns true if there
    /// were no errors.
    bool validate() {
      // If the field has not been touched yet, the field is validated
      if(!touched) {
        return true;
      }
  
      setState(() {
        _validate();
      });
      return !hasError;
    }
  
    void _validate() {
      // TODO: write a real validator
      if (_controller.text.isEmpty &amp;&amp; widget.required) {
        _errorText = 'Field is required';
      } else {
        _errorText = null;
      }
    }
  
    @override
    Widget build(BuildContext context) {
      register(context);
      // Widgets for the column
      List&lt;Widget&gt; children = &lt;Widget&gt;[
        _inputLabel(),
        _decoratedField(),
        _errorLabel()
      ];
  
      // Adds a divider if neeed to the column
      if (widget.withDivider) {
        children.add(Divider(height: 40.0));
      }
  
      // Renders the column
      return Column(
          crossAxisAlignment: CrossAxisAlignment.start, children: children);
    }
  
    @override
    void dispose() {
      // Clean up the controller when the widget is disposed.
      _controller.dispose();
      super.dispose();
    }
  
    @override
    void deactivate() {
      unregister(context);
      super.deactivate();
    }
  }
  
  abstract class CustomFormFieldState {
    String errorText;
  
    /// True if this field has any validation errors.
    bool get hasError =&gt; errorText != null;
  
    bool touched = false;
  
    void register(BuildContext context) {
      CustomForm.of(context)?.register(this);
    }
  
    void unregister(BuildContext context) {
      CustomForm.of(context)?.unregister(this);
    }
  
    void save();
    void reset();
    bool validate();
  }
  
  </code></pre>