Общие ошибки с TextFormFields в Flutter

Общие ошибки с TextFormFields в Flutter

FlutterPulse

Эта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Когда дело доходит до мобильной разработки, даже небольшие ошибки в проектировании форм могут существенно повлиять не только на пользовательский опыт, но и на конверсию приложения. Неправильно настроенные поля ввода вызывают раздражение пользователей и их отказ от дальнейшего взаимодействия, что в свою очередь ухудшает показатели конверсии. В этой статье мы рассмотрим распространенные ошибки при работе с TextFormFields в Flutter и исправим их.

1. Отсутствие реализации textInputAction

Чтобы сделать пользовательский поток удобным, важно правильно настроить действия ввода текста. При тестировании на компьютере может быть неочевидно, как приложение будет вести себя с виртуальной клавиатурой. Чтобы включить её в iOS Simulator, нажмите Cmd+K.

Давайте реализуем самый простой пример Form и попробуем его:

Form(
  child: Column(
    children: [
      TextFormField(
        decoration: const InputDecoration(
          labelText: 'Field 1',
        ),
      ),
      TextFormField(
        decoration: const InputDecoration(
          labelText: 'Field 2',
        ),
      ),
      TextFormField(
        decoration: const InputDecoration(
          labelText: 'Field 3',
        ),
      ),
    ],
  ),
)

Как видите, виртуальная клавиатура имеет кнопку "done", нажатие на которую скрывает клавиатуру. Как пользователь, я предпочел бы, чтобы она фокусировалась (разфокусировалась, если вы из мира веб-фронтенда) на следующем поле. К счастью, это легко реализовать:

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Field 1',
  ),
  textInputAction: TextInputAction.next, // <-- 
),

Добавьте эту строку ко всем полям формы, которые должны передавать фокус следующему:

Помимо передачи фокуса следующему полю, существуют и другие полезные действия ввода текста:

  • newline: вставляет новую строку вместо снятия фокуса с поля.
  • previous: фокусируется на предыдущем поле

Другие действия заключаются в закрытии клавиатуры и вызове колбэка onFieldSubmitted():

  • done, go, search, send

Некоторые из них поддерживаются только на iOS:

  • continueAction, emergencyCall, route, join

И есть одно действие, доступное только на Android:

Означает, что от клавиатуры не ожидается никаких действий. Клавиатура может выбрать действие, которое чаще всего будет новой строкой, однако это действие никогда не будет отправлено в колбэки IME как выполненное действие.

2. Не реализована отправка формы

Последняя область ввода в форме может вызвать callback onFieldSubmitted() и вы сэкономите пользователям несколько кликов, если основное действие будет запускаться прямо с клавиатуры. Давайте посмотрим, как это выглядит:

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Field 3',
  ),
  onFieldSubmitted: (_) {
    startLoading();
  },
),

Как видите, callback onFieldSubmitted() был вызван, когда пользователь нажал кнопку Done.

3. Неправильный тип ввода

Важно, чтобы поля ввода текста отображали правильную клавиатуру. Если это номер телефона, используйте keyboardType: TextInputType.phoneNumber, если это email, используйте keyboardType: TextInputType.emailAddress, и т.д..

К сожалению, Flutter пока не поддерживает подсказки для выбора языка на экране клавиатуры, как в нативной разработке под Android, однако вы можете проверить этот issue на GitHub.

4. Не использование TextCapitalization

Для некоторых полей мы ожидаем, что они должны начинаться с заглавной буквы, если это заголовок или сообщение, для некоторых, наоборот, мы предпочитаем, чтобы они начинались со строчной, или, если это имя, каждое новое слово должно начинаться с заглавной буквы. Это довольно просто реализовать в Flutter.

Давайте используем 3 разных стратегии капитализации и посмотрим на результат:

// первое поле
textCapitalization: TextCapitalization.words,
// второе поле
textCapitalization: TextCapitalization.characters,
maxLength: 6,
// третье поле
textCapitalization: TextCapitalization.sentences,

5. Не использование TextInputFormatter

Flutter предоставляет удобный способ форматирования ввода. Представьте такие сценарии: ввод номера карты, лицензионного ключа и т.д.

Также можно фильтровать определенные символы из ввода, например, если вы хотите запретить использование эмодзи в поле имени, вы можете либо валидировать при отправке, либо просто не разрешать это так:

TextFormField(
  inputFormatters: [FilteringTextInputFormatter.deny(emojiRegex)],
  decoration: const InputDecoration(
    labelText: 'Field 3',
  ),
),

Обратите внимание, что LLMs довольно хорошо генерируют регулярные выражения.

Как видите, пользователь не может вводить эмодзи. Это может быть немного удобнее, чем показывать ошибку валидации, особенно если это длинная многошаговая форма.

Вы можете узнать больше о форматировании входных данных здесь.

6. Неиспользование подсказок и групп автозаполнения

Пользователь уже вводил свое имя, email и другие поля множество раз в разных приложениях и на разных сайтах. Нет необходимости заставлять его вводить это снова и снижать конверсию. Давайте создадим форму, которая будет содержать поля для email и пароля.

Обратите внимание, что некоторые autofillHintsтребуют конкретных типов клавиатуры.

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
  autofillHints: const [AutofillHints.email], // <-- это
  keyboardType: TextInputType.emailAddress, // <-- и это
  textInputAction: TextInputAction.next,
),
TextFormField(
  keyboardType: TextInputType.visiblePassword, // <-- это
  obscureText: true, // <-- это
  autofillHints: const [AutofillHints.password], // <-- это
  inputFormatters: [
    FilteringTextInputFormatter.deny(emojiRegex), 
  ],
  decoration: const InputDecoration(
    labelText: 'Password',
  ),
),

В результате пользователю будет предложено автоматически заполнить поля.

Однако есть одна проблема. Эти email не связаны с приложением, и пароль не заполняется автоматически. Будет удобнее заполнить всё сразу. Для этого оберните всё в AutofillGroup:

Form(
  child: AutofillGroup( // <--
    child: Column(
      children: [
        TextFormField(),
        ...
      ],
    ),
  ),
),

А затем обновите колбэкonFieldSubmitted()последнего текстового поля:

TextFormField(
  ...
  decoration: const InputDecoration(
    labelText: 'Password',
  ),
  onFieldSubmitted: (value) {
    TextInput.finishAutofillContext(); // <-- это
  },
),

Когда пользователь вводит свои данные в вашем приложении, система предложит сохранить значения.

В следующий раз, когда пользователь откроет ваше приложение, сохраненная группа автозаполнения будет предложена системой:

Report Page