TextEditingController behaviour in custom text field

Issue

I composed a custom text field that check the validator when it lost focus, with some UI such as error text and green checked icon. They are working fine until I need to perform some auto-filling according to other TextField’s data.

When user filled up the postal code, the API will be called from ChangeNotifierProvider view model and update the state according

user.prefectureName = data.prefectureName;
user.city = data.city;
user.address = data.address;
notifyListeners();

In the HookConsumerWidget page, I passed the state into the custom text field like this

 OCTextField(
    text: user.prefectureName,
    labelText: l10n.accountProfileEditPrefectureName,
    errorText: "都道府県は必須項目です",
    onChanged: (value) => user.prefectureName = value,
    validator: (value) => value.isNotEmpty),
OCTextField(
    text: user.city,
    labelText: l10n.accountProfileEditCity,
    errorText: "市区町村は必須項目です",
    onChanged: (value) => user.city = value,
    validator: (value) => value.isNotEmpty),
OCTextField(
    text: user.address,
    labelText: l10n.accountProfileEditAddress,
    errorText: "丁目・番地は必須項目です",
    onChanged: (value) => user.address = value,
    validator: (value) => value.isNotEmpty),

enter image description here
However, when the state change (user.prefectureName, user.city and user.address), my custom text field won’t be able to reflect the changes accordingly.
Here is part of my custom text field code

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

import '../util/ext/string_ext.dart';

typedef Validator = bool Function(String value);

class OCTextField extends HookWidget {
  const OCTextField(
      {Key? key,
      this.text = "",
      this.labelText,
      this.hintText,
      this.errorText,
      this.maxLines = 1,
      this.maxLength,
      this.sanitise = true,
      this.onChanged,
      this.validator})
      : super(key: key);

  final String text;
  final String? labelText;
  final String? hintText;
  final String? errorText;
  final int? maxLines;
  final int? maxLength;
  final bool sanitise;
  final ValueChanged<String>? onChanged;
  final Validator? validator;

  @override
  Widget build(BuildContext context) {
    final _showError = useState(false);
    final _controller = useTextEditingController(text: text);
    final _checkIcon;
    final unchecked =
        const Icon(Icons.check_circle_outline, color: Colors.black26);
    final checked = const Icon(Icons.check_circle, color: Colors.green);
    if (validator != null) {
      _checkIcon = useState(validator!(_controller.text) ? checked : unchecked);
    } else {
      _checkIcon = useState(checked);
    }

    return Focus(
        child: TextField(
            controller: _controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(1.0),
              ),
              suffixIcon: _checkIcon.value,
              labelText: labelText,
              hintText: hintText,
              errorText: _showError.value ? errorText : null,
            ),
            maxLines: maxLines,
            maxLength: maxLength,
            onChanged: onChanged),
        onFocusChange: (isFocused) {
          if (isFocused) {
            _showError.value = false;
            _checkIcon.value = unchecked;
          } else {
            if (sanitise) {
              _controller.text = _controller.text.sanitise();
              onChanged!(_controller.text.sanitise());
            }
            if (validator != null) {
              _showError.value = !validator!(_controller.text);
            }
            _checkIcon.value = _showError.value ? unchecked : checked;
          }
        });
  }
}

I actually read the documentation of useTextEditingController(text: text) which it does not react to the text changes. I tried to replace useTextEditingController to TextEditingController and it somehow works, with some strange behaviour – Even I erased the reflected pre-filled text and move the another field, the text will come back again; The cursor is acting weird, when I click on the field, it starts from the beginning of the pre-fill text.

It works perfectly without strange issue when I inherited TextField class and initialise the TextControllerEditor directly into TextField instance.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class OutlinedTextField extends TextField {
  OutlinedTextField(
      {String? labelText,
      String? text,
      String? errorText,
      String? hintText,
      TextInputType keyboardType = TextInputType.text,
      int maxLines = 1,
      Icon? icon,
      List<TextInputFormatter>? inputFormatters,
      ValueChanged<String>? onChanged,
      isDense = false})
      : super(
          controller: text == null ? null : TextEditingController(text: text),
          decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(1.0),
              ),
              labelText: labelText,
              hintText: hintText,
              errorText: errorText,
              prefixIcon: icon,
              isDense: isDense),
          keyboardType: keyboardType,
          maxLines: maxLines,
          inputFormatters: inputFormatters,
          onChanged: onChanged,
        );
}

With this approach, I am not able to obtain the controller. The reason I am refactor the Custom Text Field with composition instead of inheritance is to access the controller.text for validation UI. Am I doing some serious mistake or misunderstanding the concept here? I am not able to figure out this strange behaviour.

Solution

  1. you have to add reusable controller in your custom textfield
  2. Note: You have to always create a seperate controller for each textfield
class CustomTextField extends StatelessWidget {

// created custom controller
  TextEditingController controller;
  String text;

   CustomTextField({
    required this.controller, 
     required this.text
  });

  @override
  Widget build(BuildContext context) {
    return TextField();
  }
}
  1. then create a textEditingController for each text Filed and initilize it in oninit function.
late TextEditingController text1 ;
  late TextEditingController text2 ;
  1. use the controllers in your custom text fields
 CustomTextField(controller: text1 ,text: "text1",),
 CustomTextField(controller: text2 ,text: "text2",)

Answered By – Sumed

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *