3
votes

I've gotten to the point of acceptance that this is just how Flutter works, but was hoping someone could explain why a physical keyboard's tab button creates a single space in a TextFormField when navigating to the next form field? I've used RawKeyboardInput to assign tab to shift the focus (which it does), but it still creates that single space in the form field.

enter image description here

Having this single space can cause a number of issues when it comes to storing the data, so I'd prefer to be able to fix it at this point than do a "string.strip()" later.

2
Any and all suggestions are greatly appreciated. - Michael Tolsma

2 Answers

5
votes

I have the same problem, while testing a form in Android emulator, and using TAB to move to the next TextFormField.

My workaround has been to wrap the TextFormField in a RawKeyboardListener.

The code below has everything you need, you can ignore "LoginStore" related things because I'm using MobX state manager.

Sample:

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

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {

  TextEditingController _userNameController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();

  FocusNode _usernameFocusNode;
  FocusNode _passwordKeyboardFocusNode;
  FocusNode _passwordFocusNode;

  LoginStore _loginStore;

  @override
  void initState() {
    super.initState();
    _passwordFocusNode = FocusNode();
    _passwordKeyboardFocusNode = FocusNode();
    _usernameFocusNode = FocusNode();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: true,
      body: Material(
        child: Stack(
          children: <Widget>[
            userNameTextField(),
            SizedBox(height: 12.0),
            passwordTextField(),
            SizedBox(height: 12.0),
            loginButton()
          ]
        )
      )
    );
  }

  Widget userNameTextField() {
    //mobx observer omitted
    return RawKeyboardListener(
      focusNode: _usernameFocusNode,
      child: TextFormField(
        decoration: InputDecoration(
          hintText: "username",
          errorText: _loginStore.formErrorStore.username
        ),
        controller: _userNameController,
        onChanged: (dynamic value) {
          _loginStore.setUserName(_userNameController.text);
        },
        onFieldSubmitted: (dynamic value) {
          FocusScope.of(context).requestFocus(_passwordFocusNode);
        }
      ),
      onKey: (RawKeyEvent event) {
        if (event.isKeyPressed(LogicalKeyboardKey.tab)) {
          var currentText = _userNameController.text;
          var textWithoutTab = currentText.replaceAll("\t", "");
          //update the controller and the store
          _userNameController.text = textWithoutTab;
          _loginStore.setUserName(_userNameController.text);
          //move the focus to the password form
          FocusScope.of(context).requestFocus(_passwordFocusNode);
        }
      }
    );
  }

  Widget passwordTextField() {
    //mobx observer omitted
    return RawKeyboardListener(
      focusNode: _passwordKeyboardFocusNode,
      child: TextFormField(
        decoration: InputDecoration(
          hintText: "password",
          errorText: _loginStore.formErrorStore.password
        ),
        controller: _passwordController,
        onChanged: (dynamic value) {
          _loginStore.setPassword(_passwordController.text);
        }
      ),
      onKey: (RawKeyEvent event) {
        if (event.isKeyPressed(LogicalKeyboardKey.tab)) {
          var currentText = _passwordController.text;
          var textWithoutTab = currentText.replaceAll("\t", "");
          //update the controller and the store
          _passwordController.text = textWithoutTab;
          _loginStore.setPassword(_passwordController.text);
        }
      }
    );
  }

  Widget loginButton() {
    //add a login button as you want ....
  }

  @override
  void dispose() {
    // Clean up the controller when the Widget is removed from the Widget tree
    _userNameController.dispose();
    _passwordController.dispose();
    _passwordKeyboardFocusNode.dispose();
    _passwordFocusNode.dispose();
    _usernameFocusNode.dispose();
    super.dispose();
  }

}

Remember that setting a text controller with a custom value like this _passwordController.text = textWithoutTab;, does not trigger onChanged callback, you need to keep the state in sync with the updated text if you perform form validation, that's why I had to perform another call to _loginStore.setPassword(_passwordController.text);

0
votes

I used function trim() in instances of TextEditingController() to remove all space.

final _controllerEmail = TextEditingController();

user.email = _controllerEmail.text.trim();

for more references function trim() https://api.dart.dev/stable/2.10.4/dart-core/String/trim.html