Как стать автором
Обновить

Работа с формами во Flutter

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров287

Формы являются фундаментальным элементом любого современного приложения. Независимо от того, создаете ли вы корпоративный портал, социальную сеть или электронную коммерцию — работа с пользовательскими данными через формы неизбежна.

В современном Flutter-приложении формы встречаются повсеместно:

  • Аутентификация — логин и регистрация

  • Профили пользователей — настройка личных данных

  • Формы обратной связи — сбор отзывов и предложений

  • Фильтры и поиск — сложные системы фильтрации

  • Анкеты и опросы — сбор структурированной информации

  • Настройки приложения — персонализация интерфейса

Базовые инструменты Flutter

Фреймворк Flutter предоставляет встроенные компоненты для работы с формами:

  • Form — основной контейнер для группы полей

  • TextFormField — базовое текстовое поле с валидацией

  • TextEditingController — управление состоянием ввода

  • GlobalKey<FormState> — доступ к состоянию формы

Эти инструменты отлично справляются с простыми задачами, но по мере роста сложности приложения возникают новые требования:

  • Масштабируемость — поддержка большого количества полей

  • Повторное использование — создание переиспользуемых компонентов

  • Асинхронная валидация — проверка данных на сервере

  • Управление состоянием — интеграция с глобальным состоянием приложения

  • Тестирование — обеспечение качества кода

Цель материала

В этой серии статей мы подробно рассмотрим различные подходы к работе с формами во Flutter, начиная с базовых инструментов и заканчивая современными решениями. Вы узнаете:

  • Как эффективно использовать встроенные возможности Flutter

  • Какие существуют альтернативные библиотеки и подходы

  • Как выбрать оптимальное решение для вашего проекта

  • Как избежать распространенных ошибок при работе с формами

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

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

Стандартные формы Flutter (Form и FormField)

Flutter предоставляет встроенный механизм для создания форм. Основные элементы — это FormFormStateTextFormField и GlobalKey.

final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();

final _passwordController = TextEditingController();
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
String? serverError;

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        controller: _emailController,
        focusNode: _emailFocus,
        decoration: InputDecoration(
          labelText: 'Email',
          errorText: serverError, // ошибка с сервера
        ),
        validator: (value) =>
            value != null && value.contains('@') ? null : 'Введите корректный email',
        onFieldSubmitted: (_) {
          FocusScope.of(context).requestFocus(_passwordFocus);
        },
      ),
      TextFormField(
        controller: _passwordController,
        focusNode: _passwordFocus,
        obscureText: true,
        decoration: InputDecoration(labelText: 'Пароль'),
        validator: (value) =>
            value != null && value.length >= 6 ? null : 'Минимум 6 символов',
      ),

        onPressed: () {
          if (_formKey.currentState!.validate()) {
            final email = _emailController.text;
            final password = _passwordController.text;
            print('Email: \$email, Password: \$password');
          }
        },
        child: Text('Войти'),
      ),
    ],
  ),
);

Как управлять фокусом между полями

final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();

TextFormField(
  focusNode: _emailFocus,
  onFieldSubmitted: (_) {
    FocusScope.of(context).requestFocus(_passwordFocus);
  },
),
TextFormField(
  focusNode: _passwordFocus,
),

Как показать ошибки после отправки (например, с сервера)

Чтобы отрисовать ошибку, которая пришла с бэкенда, можно использовать TextFormField.decoration.errorText:

String? serverError;

TextFormField(
  controller: _emailController,
  decoration: InputDecoration(
    labelText: 'Email',
    errorText: serverError,
  ),
);

// Внутри кнопки или в ответе сервера:
setState(() {
  serverError = 'Такой email не зарегистрирован';
});

Пример формы для редактирования существующих данных

Если вы загружаете данные пользователя из базы или API и хотите отобразить их в форме, используйте TextEditingController с начальными значениями:

final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'user@example.com');
final _nameController = TextEditingController(text: 'Иван Иванов');

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        controller: _emailController,
        decoration: InputDecoration(labelText: 'Email'),
      ),
      TextFormField(
        controller: _nameController,
        decoration: InputDecoration(labelText: 'Имя'),
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            final updatedEmail = _emailController.text;
            final updatedName = _nameController.text;
            print('Сохранение: \$updatedEmail, \$updatedName');
          }
        },
        child: Text('Сохранить'),
      ),
    ],
  ),
);

Кастомные поля без библиотек

Иногда нужно создавать свои собственные виджеты, которые не являются частью стандартного набора TextFormField или DropdownButtonFormField. Например, переключатели, селекторы даты, или даже составные поля вроде выбора телефона и страны.Вот пример, как создать кастомное поле, совместимое с формой:

class CustomCheckboxFormField extends FormField<bool> {
  CustomCheckboxFormField({
    Key? key,
    required Widget title,
    required FormFieldSetter<bool> onSaved,
    FormFieldValidator<bool>? validator,
    bool initialValue = false,
    AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
  }) : super(
          key: key,
          onSaved: onSaved,
          validator: validator,
          initialValue: initialValue,
          autovalidateMode: autovalidateMode,
          builder: (FormFieldState<bool> state) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Checkbox(
                      value: state.value,
                      onChanged: (value) {
                        state.didChange(value);
                      },
                    ),
                    title,
                  ],
                ),
                if (state.hasError)
                  Padding(
                    padding: const EdgeInsets.only(top: 5),
                    child: Text(
                      state.errorText ?? '',
                      style: TextStyle(color: Colors.red),
                    ),
                  ),
              ],
            );
          },
        );
  }

Такой подход позволяет создать переиспользуемый UI-компонент, который работает со стандартным Form, FormState, и умеет валидироваться, сохраняться и показывать ошибки.

Проверка на изменения перед выходом

Иногда нужно предупредить пользователя, если он покидает экран с несохранёнными изменениями. Пример с использованием WillPopScope:

final _initialEmail = 'user@example.com';
final _initialName = 'Иван Иванов';

final _emailController = TextEditingController(text: _initialEmail);
final _nameController = TextEditingController(text: _initialName);

WillPopScope(
  onWillPop: () async {
    final hasChanged = _emailController.text != _initialEmail ||
                       _nameController.text != _initialName;

    if (hasChanged) {
      final shouldLeave = await showDialog<bool>(
        context: context,
        builder: (ctx) => AlertDialog(
          title: Text('Вы уверены?'),
          content: Text('У вас есть несохранённые изменения. Покинуть экран?'),
          actions: [
            TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: Text('Отмена')),
            TextButton(onPressed: () => Navigator.of(ctx).pop(true), child: Text('Покинуть')),
          ],
        ),
      );
      return shouldLeave ?? false;
    }

    return true;
  },
  child: Form(
    key: _formKey,
    child: Column(
      children: [
        TextFormField(controller: _emailController, decoration: InputDecoration(labelText: 'Email')),
        TextFormField(controller: _nameController, decoration: InputDecoration(labelText: 'Имя')),
        ElevatedButton(
          onPressed: () {
            if (_formKey.currentState!.validate()) {
              print('Сохранение: \${_emailController.text}, \${_nameController.text}');
            }
          },
          child: Text('Сохранить'),
        ),
      ],
    ),
  ),
);

Чтобы отрисовать ошибку, которая пришла с бэкенда, можно использовать TextFormField.decoration.errorText:

String? serverError;

TextFormField(
  controller: _emailController,
  decoration: InputDecoration(
    labelText: 'Email',
    errorText: serverError,
  ),
);

// Внутри кнопки или в ответе сервера:
setState(() {
  serverError = 'Такой email не зарегистрирован';
});

Преимущества

  • Простая реализация и понимание

  • Поддерживается большинством базовых FormField

Недостатки:

  • Нужно вручную создавать и управлять множеством контроллеров, фокусами и состояниями ошибок

  • Неудобно обрабатывать валидационные или сетевые ошибки после отправки формы

  • Сложности при работе с состоянием через Bloc или Cubit, так как встроенные FormField не интегрируются напрямую с внешними состояниями и требуют дополнительных прослоек или обёрток

  • Неудобно расширять или переиспользовать составные поля без написания кастомных FormField

Работа с формой без TextEditingController

Вместо использования TextEditingController можно задать initialValue и собрать данные через onSaved:

String? email;

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        initialValue: 'user@example.com',
        decoration: InputDecoration(labelText: 'Email'),
        validator: (value) =>
            value != null && value.contains('@') ? null : 'Некорректный email',
        onSaved: (value) => email = value,
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            _formKey.currentState!.save();
            print('Email: \$email');
          }
        },
        child: Text('Сохранить'),
      ),
    ],
  ),
);

Плюсы подхода без контроллеров

  • Меньше кода: не нужно создавать и очищать контроллеры вручную

  • Удобно, если данные нужны только в момент отправки

Минусы

  • Нельзя изменить значение поля после инициализации

  • Нет прямого доступа к текущему значению вне onSaved

  • Не получится отслеживать изменения текста в реальном времени

Жизненный цикл TextEditingController и FocusNode

Если вы всё же используете контроллеры и FocusNode, не забудьте их очищать в dispose():

@override
void dispose() {
  _emailController.dispose();
  _passwordController.dispose();
  _emailFocus.dispose();
  _passwordFocus.dispose();
  super.dispose();
}

Минусы:

  • Нужно вручную создавать и управлять множеством контроллеров, фокусами и состояниями ошибок

  • Неудобно обрабатывать валидационные или сетевые ошибки после отправки формы

  • Сложности при работе с состоянием через Bloc или Cubit, так как встроенные FormField не интегрируются напрямую с внешними состояниями и требуют дополнительных прослоек или обёрток

  • Неудобно расширять или переиспользовать составные поля без написания кастомных FormField

  • Сложно расширять

  • Нельзя использовать произвольные виджеты без оборачивания в FormField

  • Неудобно работать с фокусом, асинхронной валидацией и обновлением состояния

Тестирование форм

Надёжное тестирование форм помогает убедиться, что ваша логика валидации, обработки и отображения ошибок работает корректно. Во Flutter можно использовать несколько уровней тестирования:

testWidgets('Form shows error on invalid email', (tester) async {
  final formKey = GlobalKey<FormState>();

  await tester.pumpWidget(
    MaterialApp(
      home: Form(
        key: formKey,
        child: TextFormField(
          validator: (val) => val!.contains('@') ? null : 'Invalid email',
        ),
      ),
    ),
  );

  await tester.enterText(find.byType(TextFormField), 'notanemail');
  formKey.currentState!.validate();
  await tester.pump();

  expect(find.text('Invalid email'), findsOneWidget);
});

Тестирование бизнес-логики отдельно от UI

Если вы используете Bloc, ChangeNotifier, ValueNotifier, то можно покрыть тестами логику формы отдельно от интерфейса:

blocTest<LoginBloc, LoginState>(
  'emits new state with updated email',
  build: () => LoginBloc(),
  act: (bloc) => bloc.add(EmailChanged('user@example.com')),
  expect: () => [LoginState(email: 'user@example.com')],
);

Интеграционные тесты (end-to-end)

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

testWidgets('Login flow', (tester) async {
  await tester.pumpWidget(MyApp());

  await tester.enterText(find.byKey(Key('emailField')), 'user@example.com');
  await tester.enterText(find.byKey(Key('passwordField')), 'secret');
  await tester.tap(find.byKey(Key('loginButton')));
  await tester.pumpAndSettle();

  expect(find.text('Добро пожаловать'), findsOneWidget);
});

Юнит-тесты для валидации

Если валидаторы вынесены в отдельные функции, их можно протестировать напрямую:

String? emailValidator(String? val) {
  if (val == null || !val.contains('@')) return 'Invalid email';
  return null;
}

test('email validator returns error for invalid email', () {
  expect(emailValidator('nope'), 'Invalid email');
  expect(emailValidator('test@mail.com'), null);
});

В ходе анализа базовых инструментов Flutter для работы с формами мы выявили ключевые проблемы, с которыми сталкиваются разработчики:

  • Сложность масштабирования — необходимость создания множества контроллеров и узлов фокуса

  • Неудобство асинхронной валидации — отсутствие встроенной поддержки асинхронных проверок

  • Проблемы интеграции с системами управления состоянием

  • Избыточность кода при работе со сложными формами

Что ждёт впереди

В последующих частях мы подробно разберём такие инструменты, как go_formreactive_forms и другие популярные решения, которые:

  • Автоматизируют рутинные задачи

  • Упростят работу с асинхронной валидацией

  • Предоставят готовые решения для сложных форм

  • Повысят производительность разработки

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

Тг автора: kotelnikoff_dev

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
0
Комментарии0

Публикации

Ближайшие события