2

On focus, my textFormFields or textFields trigger unnecessary rebuilds, spike resource drains & make emulator & computer slow or even unusable. I've done considerable research on this. It seems like quite an issue & I haven't been able to resolve it.

What I've tried:

  • making form key a static final
    => static final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>();

  • using sizer instead of MediaQuery for dimensions

  • played around with calling field from ConsumerWidget, StatefulWidget, StatelessWidget, & ConsumerStatefulWidget

Nothing has worked so far.
Is there something I'm doing wrong?
Or is this the Flutter life of heated state management issues galore X 60hz refresh resource drain? :(

Here is the form field:

class CustomFormField extends StatefulWidget {
  const CustomFormField({
    Key? key,
    required TextEditingController controller,
    required bool prefixCurrencySymbol,
    required String? currencySymbol,
    required List<String> autoFillHints,
    required bool blockSystemKeyboard,
    required double width,
    required FocusNode focusNode,
    required TextInputType keyboardType,
    required TextInputAction inputAction,
    required String label,
    required TextCapitalization textCapitalization,
    required Function(String value) validator,
    required bool obscurePasswordOption,
    required bool saveAutofillData,
    required bool passwordCreationField,
    required Future<void> Function(String?)? onChanged,
  })  : _formController = controller,
        _prefixCurrencySymbol = prefixCurrencySymbol,
        _currencySymbol = currencySymbol,
        _autoFillHints = autoFillHints,
        _blockSystemKeyboard = blockSystemKeyboard,
        _width = width,
        _formFocusNode = focusNode,
        _keyboardType = keyboardType,
        _inputAction = inputAction,
        _textCapitalization = textCapitalization,
        _label = label,
        _validator = validator,
        _obscurePasswordOption = obscurePasswordOption,
        _saveAutofillData = saveAutofillData,
        _passwordCreationField = passwordCreationField,
        _onChanged = onChanged,
        super(key: key);

  final TextEditingController _formController;
  final bool _prefixCurrencySymbol;
  final String? _currencySymbol;
  final List<String> _autoFillHints;
  final bool _blockSystemKeyboard;
  final double _width;
  final FocusNode _formFocusNode;
  final TextInputType _keyboardType;
  final TextInputAction _inputAction;
  final String _label;
  final bool _obscurePasswordOption;
  final TextCapitalization _textCapitalization;
  final Function(String) _validator;
  final bool _saveAutofillData;
  final bool _passwordCreationField;
  final Future<void> Function(String?)? _onChanged;

  @override
  State<CustomFormField> createState() => _CustomFormFieldState();
}

class _CustomFormFieldState extends State<CustomFormField> {
  bool _obscurePassword = true;

  @override
  void dispose() {
    widget._formFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget._width,
      child: TextFormField(
        style: ThemeEndpoints.textFieldTextStyle(),
        autofillHints: widget._autoFillHints,
        enableInteractiveSelection: true,
        enableSuggestions: false,
        autocorrect: false,
        textAlignVertical: const TextAlignVertical(y: 0.1),
        readOnly: widget._blockSystemKeyboard,
        maxLines: 1,
        controller: widget._formController,
        focusNode: widget._formFocusNode,
        keyboardType: widget._keyboardType,
        obscureText: (widget._obscurePasswordOption) ? _obscurePassword : false,
        textCapitalization: widget._textCapitalization,
        textInputAction: widget._inputAction,
        validator: (text) => widget._validator(text!),
        onChanged: (text) => (widget._passwordCreationField && text.isNotEmpty)
            ? widget._onChanged!(text)
            : null,
        onEditingComplete:
            (widget._saveAutofillData) ? () => TextInput.finishAutofillContext() : null,
        toolbarOptions: const ToolbarOptions(
          copy: true,
          paste: true,
          cut: true,
          selectAll: true,
        ),
        decoration: InputDecoration(
          filled: true,
          fillColor: ThemeEndpoints.textFieldBackgroundColor(),
          alignLabelWithHint: true,
          labelText: widget._label,
          labelStyle: ThemeEndpoints.textFieldLabelStyle(),
          contentPadding: const EdgeInsets.fromLTRB(32, 24, 12, 16),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(30.0),
            borderSide: const BorderSide(
              width: 0,
              style: BorderStyle.none,
            ),
          ),
          errorStyle: ThemeEndpoints.textFieldErrorTextStyle(),
          errorBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(30.0),
            borderSide: const BorderSide(
              color: BrandColors.appLightRed,
              width: 2,
            ),
          ),
          focusedErrorBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(30.0),
            borderSide: const BorderSide(
              color: BrandColors.appRed,
              width: 2,
            ),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(30.0),
            borderSide: const BorderSide(
              color: BrandColors.luckyLime,
              width: 2,
            ),
          ),
          prefixIconConstraints: (widget._prefixCurrencySymbol)
              ? const BoxConstraints(minWidth: 0, minHeight: 0)
              : null,
          prefixIcon: (widget._prefixCurrencySymbol)
              ? Container(
                  margin: const EdgeInsets.all(13),
                  child: Text(
                    widget._currencySymbol!,
                    style: ThemeEndpoints.textFieldCurrencySymbolTextStyle(),
                  ),
                )
              : null,
          suffixIcon: (widget._obscurePasswordOption)
              ? GestureDetector(
                  onTap: () {
                    setState(() {
                      _obscurePassword = !_obscurePassword;
                    });
                  },
                  child: Container(
                    margin: const EdgeInsets.all(13),
                    child: _obscurePassword
                        ? ThemeEndpoints.textFieldPasswordNotVisible()
                        : ThemeEndpoints.textFieldPasswordVisible(),
                  ),
                )
              : null,
        ),
      ),
    );
  }
}

Here is my Form:

class SignInForm extends ConsumerWidget {
  final FocusNode emailFocusNode;
  final FocusNode passwordFocusNode;

  SignInForm({
    Key? key,
    required this.emailFocusNode,
    required this.passwordFocusNode,
  }) : super(key: key);

  final TextEditingController _email = TextEditingController();
  final TextEditingController _password = TextEditingController();
  static final GlobalKey<FormState> _signInFormKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return AutofillGroup(
      child: Form(
        key: _signInFormKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              TextContent.of(context).authSignInText,
              style: ThemeEndpoints.primaryHeader(),
            ),
            const SizedBox(height: 16.0),
            CustomFormField(
              controller: _email,
              onChanged: null,
              prefixCurrencySymbol: false,
              currencySymbol: null,
              autoFillHints: const [AutofillHints.email],
              blockSystemKeyboard: false,
              width: 90.w,
              focusNode: emailFocusNode,
              keyboardType: TextInputType.emailAddress,
              inputAction: TextInputAction.next,
              label: TextContent.of(context).authFormEmailText,
              textCapitalization: TextCapitalization.none,
              validator: (email) => FormValidationUtility.emailFormValidator(
                context,
                email,
              ),
              obscurePasswordOption: false,
              saveAutofillData: false,
              passwordCreationField: false,
            ),
            const SizedBox(height: 16.0),
            CustomFormField(
              controller: _password,
              onChanged: null,
              prefixCurrencySymbol: false,
              currencySymbol: null,
              autoFillHints: const [AutofillHints.password],
              blockSystemKeyboard: false,
              width: 90.w,
              focusNode: passwordFocusNode,
              keyboardType: TextInputType.visiblePassword,
              inputAction: TextInputAction.done,
              label: TextContent.of(context).authFormPasswordText,
              textCapitalization: TextCapitalization.none,
              validator: (password) => FormValidationUtility.passwordGeneralValidator(
                context,
                password,
              ),
              obscurePasswordOption: true,
              saveAutofillData: false,
              passwordCreationField: false,
            ),
            const SizedBox(height: 64.0),
            CustomButton(
              buttonText: TextContent.of(context).authSignInText,
              width: 50.w,
              onPressed: () async => _onSignInPressed(
                context,
                ref,
                [emailFocusNode, passwordFocusNode],
              ),
            ),
            const SizedBox(height: 16.0),
            const SignUpInkwell(),
            const SizedBox(height: 16.0),
            const SignInOptionsPulldown(),
          ],
        ),
      ),
    );
  }

  // On pressed button execution
  Future<void> _onSignInPressed(
    BuildContext context,
    WidgetRef ref,
    List<FocusNode> focusNodes,
  ) async {
    for (FocusNode node in focusNodes) {
      node.unfocus();
    }
    if (_signInFormKey.currentState!.validate()) {
      await ref.read(authenticationEndpoints).signIn(
            context: context,
            email: _email.text.trim(),
            password: _password.text.trim(),
          );
    }
  }
}
RobbB
  • 1,214
  • 11
  • 39
  • Did you manage to resolve this issue? I have the exact problem but I cant find a definitive solution. – prueba prueba May 21 '23 at 17:09
  • I believe it was fixed by Flutter. I dont get the resource drain when textField is focused anymore. Try upgrading Flutter to latest version... if that dosent fix it than I have no idea, sorry. – RobbB May 21 '23 at 23:03

1 Answers1

0

I just faced this issue last week, and found long discussion on github issue. but im forget to mark the issue, so i cant refer it.

there are 2 point that i get:

  • everytime focus triggered, the keyboard will appear on screen. this will trigger the screen to rebuild. Since numbers of the widget on screen was changed

  • there are behaviors in flutter, that when we have a nested StatefullWidget. here the question related: https://stackoverflow.com/a/50584602/12838877

in your case, you have parent statefullwidget, and some CustomTextField which is a statefull widget.


my workaround for my issue last week:

  1. to avoid rebuild unnecessary widget every focus changed, i try to minimize use local method for all my widget. i choose to wrap into new StatelessWidget or StatefullWidget. This will not rebuild your entire widget tree.
  2. then for the second problem, my solution is for every component that extends to StatefullWidget, i assign specific valueKey. even when i call setState and re-execute build method, since my valueKey is same, the widget will not rebuild.

like below

 CustomFormField(
   key: ValueKey('emailform'), 
   controller: _email,
   onChanged: null,
   ....),
CustomFormField(
   key: ValueKey('pswdForm'), 
   controller: pswd`,
   onChanged: null,
  ....)
pmatatias
  • 3,491
  • 3
  • 10
  • 30
  • Can you please explain some more with examples or tutorials how you're using keys? I would like to try this solution but I'm not sure how to pass the keys in the parent widget. Are you just declaring the keys like that and it works or is there more to the picture? I've not used keys except form validation keys so I need some more help to use this solution :) – RobbB Oct 09 '22 at 18:48
  • Tried keys, many different configurations. Does not work. – RobbB Oct 09 '22 at 22:16