Виджеты. Текстовые поля и Формы

Подготовка проекта

Сейчас освежим свои знания по вёрстке

Переходим по ссылке на GitFlic и скачиваем себе проект с роллами - https://gitflic.ru/project/igarett/course_flutter_sushi_app?branch=4.7-sushi-app-part-1

скачиваем проект с роллами

В прошлом курсе мы сделали:

Сейчас изучим более подробно про текстовые поля и создания форм во Flutter

Сделаем форму входа в наше приложение

Текстовые поля и Формы

  1. Открываем проект через VS Code или Android Studio
  2. В директории lib создаём директорию examples и добавим новый файл с названием text_field.dart

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

В этом уроке мы научимся создавать текстовые поля, управлять их состоянием, стилизовать, добавлять валидацию и объединять в полноценные формы.

Скопируйте этот код заготовку для запуска проекта

Файл text_field.dart - Заготовка
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
void main() => runApp(MyApp());  
  
class MyApp extends StatelessWidget {  
  const MyApp({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return MaterialApp(  
      title: "Flutter Course 2025",  
      debugShowCheckedModeBanner: false,  
      theme: ThemeData(  
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),  
        fontFamily: "Montserrat",  
      ),  
      home: Scaffold(body: HomeWidget()),  
    );  
  }  
}  
  
class HomeWidget extends StatelessWidget {  
  const HomeWidget({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return Container(  
      width: double.infinity,  
      decoration: BoxDecoration(  
        gradient: LinearGradient(  
          colors: [Color(0xFFBFF098), Color(0xFF6FD6FF)],  
          begin: Alignment.topLeft,  
          end: Alignment.bottomRight,  
        ),  
      ),  
      child: SafeArea(  
        child: Center(  
          child: Padding(  
            padding: const EdgeInsets.all(32.0),  
            child: TextFieldExample(),  // ← Сюда добавить нужный виджет для отображения 
          ),  
        ),  
      ),  
    );  
  }  
}

Виджет TextField

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

Файл text_field.dart - виджет TextFieldExample
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
class TextFieldExample extends StatefulWidget {  
  const TextFieldExample({super.key});  
  @override  
  State createState() => _TextFieldExampleState();  
}  
  
class _TextFieldExampleState extends State { 
@c_start_1
  // Контроллер для TextField  
@c_end_1
@c_start_2
  final TextEditingController _textController = TextEditingController();  
@c_end_2
  	
@c_start_3
  // Очищаем контроллер, когда виджет удаляется  
@c_end_3
@c_start_4
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
@c_end_4
  
  @override  
  Widget build(BuildContext context) {  
    // Используем ListView для прокрутки  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
@c_start_5
        Text("Обычное текстовое поле:", style: TextStyle(fontSize: 18)),  
@c_end_5
@c_start_6
        // 🠊 ДОБАВЛЯЕМ ТЕКСТОВОЕ ПОЛЕ
@c_end_6
@c_start_7
        TextField( 
          // 🠊 Привязываем контроллер к полю
          controller: _textController,   
        ),  
        SizedBox(height: 20),  
@c_end_7
@c_start_8
        // Кнопка для демонстрации чтения текста из контроллера  
@c_end_8
@c_start_9
        ElevatedButton(  
          onPressed: () {  
            ScaffoldMessenger.of(context).showSnackBar(  
              SnackBar(  
                content: Text(  
                  'Текст: ${_textController.text}. Символов: ${_textController.text.length}',  
                ),  
              ),  
            );  
          },  
          child: Text('Сколько символов?'),  
        ),  
@c_end_9
      ],  
    );  
  }  
}

Тип клавиатуры и методы колбэки

Укажем тип клавиатуры и методы колбэки

Укажем тип клавиатуры через свойство keyboardType: TextInputType. Очень полезное свойство, в зависимости от контекста использования текстового поля, показать пользователю, более удобную клавиатуру, например, где есть только цифры, или где есть текст и символ @ и т.п. Пользователи будут благодарны за такое внимание к деталям в нашем приложении.

Добавим методы onChanged и onSubmitted, первый получает каждый введённый символ, второй срабатывает после нажатия на Enter/Готово на клавиатуре

Файл text_field.dart - тип клавиатуры и методы колбэки
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
class TextFieldExample extends StatefulWidget {  
  const TextFieldExample({super.key});  
  @override  
  State createState() => _TextFieldExampleState();  
}  
  
class _TextFieldExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
    
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
  @override  
  Widget build(BuildContext context) {
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text("Обычное текстовое поле:", style: TextStyle(fontSize: 18)),  
        TextField(  
          controller: _textController,
          // 🠆 ДОБАВЛЯЕМ ТИП КЛАВИАТУРЫ И КОЛБЭКИ
@c_start_1
          // Тип текстовой клавиатуры
          keyboardType: TextInputType.text,   
@c_end_1
@c_start_2
          onChanged: (value) {  
            // Этот колбэк вызывается при каждом изменении текста  
            print("onChanged: $value");  
          },  
@c_end_2
@c_start_3
          onSubmitted: (value) {  
            // Вызывается после нажатия Enter/Готово на клавиатуре  
            print("onSubmitted: $value");  
          },  
@c_end_3
        ),  
        
        SizedBox(height: 20),  
        ElevatedButton(  
          onPressed: () {  
            ScaffoldMessenger.of(context).showSnackBar(  
              SnackBar(  
                content: Text(  
                  'Текст: ${_textController.text}. 
                  Символов: ${_textController.text.length}',  
                ),  
              ),  
            );  
          },  
          child: Text('Сколько символов?'),  
        ),  
      ],  
    );  
  }  
} 

Оформление текстового поля

Добавим оформление текстового поля с помощью свойства decoration и виджета InputDecoration()

Этот виджет позволяет настроить:

Файл text_field.dart - оформление текстового поля
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
class TextFieldExample extends StatefulWidget {  
  const TextFieldExample({super.key});  
  @override  
  State createState() => _TextFieldExampleState();  
}  
  
class _TextFieldExampleState extends State { 
  final TextEditingController _textController = TextEditingController();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text("Обычное текстовое поле:", style: TextStyle(fontSize: 18)),  
        TextField(  
          controller: _textController,  
          // 🠆 ДОБАВЛЯЕМ ОФОРМЛЕНИЕ
@c_start_empty_1
          decoration: InputDecoration( 
@c_start_empty_1_1
            labelText: 'Введите текст', // Плавающая метка 
@c_end_empty_1_1 
@c_start_empty_1_2 
            hintText: 'Например, Привет Flutter', // Подсказка 
@c_end_empty_1_2 
@c_start_empty_1_3 
            border: OutlineInputBorder(), // Простая рамка вокруг поля 
@c_end_empty_1_3 
          ), 
@c_end_empty_1
          keyboardType: TextInputType.text,  
          onChanged: (value) {  
            print("onChanged: $value");  
          },  
          onSubmitted: (value) {  
            print("onSubmitted: $value");  
          },  
        ),  
        SizedBox(height: 20),  
        ElevatedButton(  
          onPressed: () {  
            ScaffoldMessenger.of(context).showSnackBar(  
              SnackBar(  
                content: Text(  
                    'Текст: ${_textController.text}. 
                     Символов: ${_textController.text.length}',  
                ),  
              ),  
            );  
          },  
          child: Text('Сколько символов?'),  
        ),  
      ],  
    );  
  }  
}

Показать или Скрыть текст

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

Файл text_field.dart - скрыть пароль
Светлая тема Темная тема
class PasswordField extends StatefulWidget {  
  const PasswordField({super.key});  
  
  @override  
  State createState() => _PasswordFieldState();  
}  
  
class _PasswordFieldState extends State {  
  @c_start_1
  bool _obscureText = true; // Начальное состояние - текст скрыт  
  @c_end_1
  
  @c_start_2
  // Добавляем метод для изменения состояния
  @c_end_2
  @c_start_3
  void _togglePasswordVisibility() {  
    // Изменяем состояние при нажатии на иконку
    setState(() {  
      _obscureText = !_obscureText;  
    });  
  }  
  @c_end_3
  
  @override  
  Widget build(BuildContext context) {  
    return TextField(
@c_start_4
      obscureText: _obscureText, // Скрывает текст, если true  
@c_end_4
@c_start_5
      decoration: InputDecoration(  
        labelText: 'Пароль',  
        hintText: 'Введите ваш пароль',  
        border: OutlineInputBorder(),  
@c_end_5
@c_start_6
        suffixIcon: IconButton(  
          icon: Icon(  
            // Меняем иконку в зависимости от состояния _obscureText  
            _obscureText ? Icons.visibility_off : Icons.visibility,  
          ),  
          onPressed: _togglePasswordVisibility, // Вызываем метод при нажатии  
        ),  
      ),  
@c_end_6
    );  
  }  
}

Необходимые свойства InputDecoration

Все необходимые свойства InputDecoration для оформления:

Комплексный демонстрационный пример использования различных свойств InputDecoration

Файл text_field.dart
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
class TextFieldExample extends StatefulWidget {  
  const TextFieldExample({super.key});  
  @override  
  State createState() => _TextFieldExampleState();  
}  
  
class _TextFieldExampleState extends State {
  final TextEditingController _textController = TextEditingController();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text("Обычное текстовое поле:", style: TextStyle(fontSize: 18)),  
        TextField(  
          controller: _textController,  
          decoration: InputDecoration(  
            // 🠆 Метки и подсказки
            labelText: 'Полное имя', // Плавающая метка  
            labelStyle: TextStyle(color: Colors.black), // Стиль метки  
            hintText: 'Введите ваше имя и фамилию', // Подсказка внутри поля  
            // Стиль подсказки 
            hintStyle: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey), 
            // Вспомогательный текст под полем
            helperText: 'Введите имя, как в паспорте',   
            // Стиль вспомогательного текста 
            helperStyle: TextStyle(fontSize: 12, color: Colors.black54),  
            // Стиль текста ошибки  
            errorStyle: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), 
  
            // 🠆 Иконки
            // icon: Icon(Icons.person_outline, color: Colors.purple), 
            // Иконка слева снаружи границы  
            // Иконка слева внутри границы  
            prefixIcon: Icon(Icons.person, color: Colors.black), 
            suffixIcon: IconButton( // Иконка справа внутри границы 
              icon: Icon(Icons.clear, color: Colors.redAccent),  
              onPressed: () {  
                _textController.clear(); // Очистка поля по нажатию на иконку  
              },  
            ),  
            
            // 🠆 Префикс и Суффикс (текст)
            prefixText: 'Имя: ', // Текст перед вводимым значением 
            prefixStyle: TextStyle(fontWeight: FontWeight.bold), // Стиль префикса 
            suffixText: ' ФИО', // Текст после вводимого значения 
            suffixStyle: TextStyle(color: Colors.blue), // Стиль суффикса  
  
            // 🠆 Счетчик символов      
            // Используется вместе со свойством maxLength у TextField           
            // counterStyle: TextStyle(color: Colors.orange),  
            // Можно задать свой виджет счетчика  
            // counter: Text('Custom Counter'), 
  
            // 🠆 Границы
            // border: OutlineInputBorder(), 
            // Единая граница для всех состояний   
                     
            // Граница в активном состоянии (не в фокусе) 
            enabledBorder: OutlineInputBorder( 
              borderSide: BorderSide(color: Colors.white, width: 1.5),  
              borderRadius: BorderRadius.circular(8.0),  
            ),  
            // Граница в активном состоянии (в фокусе)  
            focusedBorder: OutlineInputBorder( 
              borderSide: BorderSide(color: Colors.green, width: 2.5),  
              borderRadius: BorderRadius.circular(8.0),  
            ),  
            // Граница при ошибке (не в фокусе)
            errorBorder: OutlineInputBorder(   
              borderSide: BorderSide(color: Colors.red, width: 1.5),  
              borderRadius: BorderRadius.circular(8.0),  
            ),  
            // Граница при ошибке (в фокусе)  
            focusedErrorBorder: OutlineInputBorder( 
              borderSide: BorderSide(color: Colors.red, width: 2.5),  
              borderRadius: BorderRadius.circular(8.0),  
            ),  
            // Граница в неактивном состоянии (onChanged: null)
            disabledBorder: OutlineInputBorder( 
              borderSide: BorderSide(color: Colors.grey.shade300, width: 1.5),  
              borderRadius: BorderRadius.circular(8.0),  
            ),  
            // underlineInputBorder(), 
            // Другой стиль границы по умолчанию (подчеркивание)  
  
            // 🠆 Отступы и плотность
            isDense: true, 
            // Уменьшает вертикальные отступы, делая поле более компактным  
            contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 15), 
            // Ручная настройка внутренних отступов  
  
            // 🠆 Другие настройки       
            alignLabelWithHint: true, 
            // Выравнивает плавающую метку с подсказкой, когда они обе видны 
            // constraints: BoxConstraints(maxHeight: 60), 
            // Ограничения размера контейнера InputDecoration  
            // Цвет заливки 
            filled: true,  
            fillColor: Colors.white,  
          ),  
          keyboardType: TextInputType.text,  
          maxLength: 50, // Ограничение символов (работает со счетчиком)  
          // onChanged: (value) { ... },          
          // onSubmitted: (value) { ... },        ),  
        SizedBox(height: 20),  
        ElevatedButton(  
          onPressed: () {  
            ScaffoldMessenger.of(context).showSnackBar(  
              SnackBar(  
                content: Text(  
                    'Текст: ${_textController.text}. 
                     Символов: ${_textController.text.length}',  
                ),  
              ),  
            );  
          },  
          child: Text('Сколько символов?'),  
        ),  
      ],  
    );  
  }  
}

Виджет TextFormField

Более серьезный вариант - это использование форм и виджета TextFormField

В данных уроках много кода. Чтобы не перегружать вас текстом и сделать объяснения максимально понятными, каждый новый фрагмент кода подробно прокомментирован.

Добавляем новый файл text_field_form.dart

Файл text_form_field.dart (ШАГИ 0–6)
Светлая тема Темная тема
// ----- ШАГ 0 -----
import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  



@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  

    ],  
  );  
}  
}

// ----- ШАГ 1 -----
import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  

    ],  
  );  
}  
}

// ----- ШАГ 2 -----
import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        
      )

    ],  
  );  
}  
}


// ----- ШАГ 3 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        
      )
    ],  
  );  
}  
}

// ----- ШАГ 4 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        // 4 Добавляем колонку 
        child: Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [  

          ],  
        ), 

      )
    ],  
  );  
}  
}

// ----- ШАГ 5 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        // 4 Добавляем колонку 
        child: Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [ 
            // 5 Добавляем TextFormField
            TextFormField(
              
            
            )

          ],  
        ), 

      )
    ],  
  );  
}  
}


// ----- ШАГ 6 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        // 4 Добавляем колонку 
        child: Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [ 
            // 5 Добавляем TextFormField
            TextFormField(
              // 6 Добавляем оформление, тип клавиатуры и обработчик события
              decoration: InputDecoration(  
                labelText: 'Имя', 
                hintText: 'Введите ваше имя',
                border: OutlineInputBorder(),  
              ),  

            )
          ],  
        ), 

      )
    ],  
  );  
}  
}

// ----- ШАГ 7 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        // 4 Добавляем колонку 
        child: Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [ 
            // 5 Добавляем TextFormField
            TextFormField(
              // 6 Добавляем оформление, тип клавиатуры и обработчик события
              decoration: InputDecoration(  
                labelText: 'Имя', 
                hintText: 'Введите ваше имя',
                border: OutlineInputBorder(),  
              ),  
              keyboardType: TextInputType.name,   


            )
          ],  
        ), 

      )
    ],  
  );  
}  
}

// ----- ШАГ 8 -----

import 'package:flutter/material.dart';  

class TextFieldFormExample extends StatefulWidget {  
const TextFieldFormExample({super.key});  

@override  
State createState() => _TextFieldFormExampleState();  
}  

class _TextFieldFormExampleState extends State {  
// Контроллер для управления текстовым полем: получения и изменения его значения
final TextEditingController _textController = TextEditingController();  

// 1 Добавляем глобьльный ключ
// Глобальный ключ нужен для идентификации и управления состоянием нашей формы
// Позволяет нам проверять (валидировать) и сохранять все поля формы одновременно 
final _formKey = GlobalKey(); 

@override  
void dispose() {  
  // Освобождаем ресурсы контроллера, когда виджет удаляется из дерева
  _textController.dispose();  
  super.dispose();  
}  

@override  
Widget build(BuildContext context) {  
  return ListView(  
    padding: const EdgeInsets.all(16.0),  
    children: [  
      // 2 Добавляем форму
      Form(
        key: _formKey, // 3 Привязываем глобальный ключ к форме

        // 4 Добавляем колонку 
        child: Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [ 
            // 5 Добавляем TextFormField
            TextFormField(
              // 6 Добавляем оформление, тип клавиатуры и обработчик события
              decoration: InputDecoration(  
                labelText: 'Имя', 
                hintText: 'Введите ваше имя',
                border: OutlineInputBorder(),  
              ),  
              keyboardType: TextInputType.name,   

              onFieldSubmitted: (value) {  }
            )
          ],  
        ), 

      )
    ],  
  );  
}  
}

Валидация формы

Добавляем обработку валидации

Файл text_field_form.dart
Светлая тема Темная тема
// ----- ШАГ 0 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  
  final _formKey = GlobalKey();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
 
  // 1 🔥 Добавляем метод для обработки отправки формы с валидацией  
  void _submitForm() {  

  }  
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey, // Привязываем глобальный ключ к форме  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя', // Метка поля  
                  hintText: 'Введите ваше имя', // Подсказка  
                  border: OutlineInputBorder(), // Граница  
                ),  
                keyboardType: TextInputType.name, // Тип клавиатуры для имени  
  
                onFieldSubmitted: (value) {  }  
  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 1 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  
  final _formKey = GlobalKey();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
 
  // 1 🔥 Добавляем метод для обработки отправки формы с валидацией  
  void _submitForm() {  
    // 2 🔥 Проверяем валидность всех полей в форме, используя ключ  
    if (_formKey.currentState!.validate()) {  
      // Если метод validate() вернул true (нет ошибок валидации), то  
      // можно выполнять дальнейшие действия  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Форма валидна!')),  
      );  
    } 
  }  
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey, // Привязываем глобальный ключ к форме  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя', // Метка поля  
                  hintText: 'Введите ваше имя', // Подсказка  
                  border: OutlineInputBorder(), // Граница  
                ),  
                keyboardType: TextInputType.name, // Тип клавиатуры для имени  
  
                onFieldSubmitted: (value) {  }  
  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 2 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  
  final _formKey = GlobalKey();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
 
  // 1 🔥 Добавляем метод для обработки отправки формы с валидацией  
  void _submitForm() {  
    // 2 🔥 Проверяем валидность всех полей в форме, используя ключ  
    if (_formKey.currentState!.validate()) {  
      // Если метод validate() вернул true (нет ошибок валидации), то  
      // можно выполнять дальнейшие действия  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Форма валидна!')),  
      );  
    } else {  
      // 3 🔥 Если validate() вернул false, ошибки уже отобразятся под полями  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      );  
    }  
  }   
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey, // Привязываем глобальный ключ к форме  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя', // Метка поля  
                  hintText: 'Введите ваше имя', // Подсказка  
                  border: OutlineInputBorder(), // Граница  
                ),  
                keyboardType: TextInputType.name, // Тип клавиатуры для имени  
  
                onFieldSubmitted: (value) {  }  
  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 3 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  
  final _formKey = GlobalKey();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
 
  // 1 🔥 Добавляем метод для обработки отправки формы с валидацией  
  void _submitForm() {  
    // 2 🔥 Проверяем валидность всех полей в форме, используя ключ  
    if (_formKey.currentState!.validate()) {  
      // Если метод validate() вернул true (нет ошибок валидации), то  
      // можно выполнять дальнейшие действия  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Форма валидна!')),  
      );  
    } else {  
      // 3 🔥 Если validate() вернул false, ошибки уже отобразятся под полями  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      );  
    }  
  }   
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey, // Привязываем глобальный ключ к форме  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя', // Метка поля  
                  hintText: 'Введите ваше имя', // Подсказка  
                  border: OutlineInputBorder(), // Граница  
                ),  
                keyboardType: TextInputType.name, // Тип клавиатуры для имени  

                // 4 🔥 Добавляем валидатор
                validator: (value) {  
                  // Валидатор получает текущее значение поля (может быть null)  
                  if (value == null || value.trim().isEmpty || value.length < 2) { 
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
  
                onFieldSubmitted: (value) {  }  
  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 4 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  
  final _formKey = GlobalKey();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
  
 
  // 1 🔥 Добавляем метод для обработки отправки формы с валидацией  
  void _submitForm() {  
    // 2 🔥 Проверяем валидность всех полей в форме, используя ключ  
    if (_formKey.currentState!.validate()) {  
      // Если метод validate() вернул true (нет ошибок валидации), то  
      // можно выполнять дальнейшие действия  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Форма валидна!')),  
      );  
    } else {  
      // 3 🔥 Если validate() вернул false, ошибки уже отобразятся под полями  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      );  
    }  
  }   
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey, // Привязываем глобальный ключ к форме  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя', // Метка поля  
                  hintText: 'Введите ваше имя', // Подсказка  
                  border: OutlineInputBorder(), // Граница  
                ),  
                keyboardType: TextInputType.name, // Тип клавиатуры для имени  

                // 4 🔥 Добавляем валидатор
                validator: (value) {  
                  // Валидатор получает текущее значение поля (может быть null)  
                  if (value == null || value.trim().isEmpty || value.length < 2) { 
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
  
                // 5 🔥 При нажатии на Enter вызываем метод _submitForm  
                onFieldSubmitted: (value) { _submitForm(); }   
  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}

Описание кода

Создаём виджет TextFieldFormExample: Это базовый виджет, который будет содержать нашу форму. Он является StatefulWidget, потому что его состояние (например, введённый текст или состояние валидации) будет меняться.

Контроллер текста (_textController): Мы используем TextEditingController, чтобы получать доступ к тексту, который пользователь вводит в поле ввода (TextFormField).

Глобальный ключ формы (_formKey): Это очень важная часть! GlobalKey позволяет нам взаимодействовать с формой в целом. Благодаря этому ключу мы можем вызвать метод validate() для всей формы и проверить все её поля одним разом.

Очистка ресурсов (dispose): Когда виджет больше не нужен, мы обязательно очищаем _textController с помощью _textController.dispose(). Это помогает избежать утечек памяти.

Метод _submitForm(): Отправка формы и валидация:

Построение интерфейса (build):

Добавим второе поле ввода

Теперь добавим второе поле ввода (для Email) и реализуем сохранение данных формы

Файл text_field_form.dart
Светлая тема Темная тема
// ----- ШАГ 0 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  


  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 1 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  

    } else { }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 2 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

    } else { }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 3 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

    } else { }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 4 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

      // Очистить форму после успешной отправки  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });

    } else { }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 5 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

      // Очистить форму после успешной отправки  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });

    } else {
        // Если форма не валидна, показываем ошибку
        ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      ); 
    }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
                
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 6 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

      // Очистить форму после успешной отправки  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });

    } else {
        // Если форма не валидна, показываем ошибку
        ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      ); 
    }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  

                // ДОБАВЛЯЕМ СОХРАНЕНИЕ ДАННЫХ
                onSaved: (newValue) {  
                  // Сохраняем значение в переменную состояния  
                  _savedName = newValue ?? '';  
                }, 
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 7 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

      // Очистить форму после успешной отправки  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });

    } else {
        // Если форма не валидна, показываем ошибку
        ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      ); 
    }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  

                // ДОБАВЛЯЕМ СОХРАНЕНИЕ ДАННЫХ
                onSaved: (newValue) {  
                  // Сохраняем значение в переменную состояния  
                  _savedName = newValue ?? '';  
                }, 
              ),  

              // ДОБАВЛЯЕМ ПОЛЕ ДЛЯ EMAIL
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Email',  
                  hintText: 'Введите ваш email',  
                  border: OutlineInputBorder(),  
                  prefixIcon: Icon(Icons.email),  
                ),  
                keyboardType: TextInputType.emailAddress, 
                 
                validator: (value) {  
                  if (value == null || value.isEmpty) {  
                    return 'Пожалуйста, введите email';  
                  }  
                  // Простая проверка формата Email  
                  if (!value.contains('@') || !value.contains('.')) {  
                    return 'Введите корректный email';  
                  }  
                  return null;  
                },  
                onSaved: (newValue) {  
                  // Сохраняем значение Email  
                  _savedEmail = newValue ?? '';  
                },  
              ), 
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}


// ----- ШАГ 8 -----
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  
  // 1 Добавим переменные для хранения данных формы  
  String _savedName = '';  
  String _savedEmail = '';  

  // 2 ДОБАВЛЯЕМ ОБРАБОТКУ ФОРМЫ И СОХРАНЕНИЕ ДАННЫХ
  
  // Метод для обработки отправки формы с валидацией и сохранением данных  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      // Если все поля валидны, вызываем метод save() у формы  
      // Это вызовет onSaved у каждого TextFormField в форме     
      _formKey.currentState!.save(); 

      // Теперь переменные _savedName и _savedEmail содержат введенные данные  
      // Можно их использовать (например, отправить на сервер)      
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content:  
              Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      ); 

      // Очистить форму после успешной отправки  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });

    } else {
        // Если форма не валидна, показываем ошибку
        ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      ); 
    }  
  }
  
  @override  
  void dispose() {  
    _textController.dispose();  
    super.dispose();  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [ 
             
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  

                // ДОБАВЛЯЕМ СОХРАНЕНИЕ ДАННЫХ
                onSaved: (newValue) {  
                  // Сохраняем значение в переменную состояния  
                  _savedName = newValue ?? '';  
                }, 
              ),  

              // ДОБАВЛЯЕМ ПОЛЕ ДЛЯ EMAIL
              TextFormField(  
                decoration: InputDecoration(  
                  labelText: 'Email',  
                  hintText: 'Введите ваш email',  
                  border: OutlineInputBorder(),  
                  prefixIcon: Icon(Icons.email),  
                ),  
                keyboardType: TextInputType.emailAddress, 
                 
                validator: (value) {  
                  if (value == null || value.isEmpty) {  
                    return 'Пожалуйста, введите email';  
                  }  
                  // Простая проверка формата Email  
                  if (!value.contains('@') || !value.contains('.')) {  
                    return 'Введите корректный email';  
                  }  
                  return null;  
                },  
                onSaved: (newValue) {  
                  // Сохраняем значение Email  
                  _savedEmail = newValue ?? '';  
                },  
              ), 

              // ДОБАВЛЯЕМ КНОПКУ ДЛЯ ОТПРАВКИ ФОРМЫ
              Center(  
                child: ElevatedButton(  
                  onPressed: _submitForm,  
                  child: Text('Отправить'),  
                ),  
              ),  
  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}

Управление фокусом

Добавим управление фокусом, чтобы улучшить взаимодействие пользователя с формой.

Файл text_field_form.dart
Светлая тема Темная тема
import 'package:flutter/material.dart';  
  
class TextFieldFormExample extends StatefulWidget {  
  const TextFieldFormExample({super.key});  
  
  @override  
  State createState() => _TextFieldFormExampleState();  
}  
  
class _TextFieldFormExampleState extends State {  
  final TextEditingController _textController = TextEditingController();  
  final _formKey = GlobalKey();  
  String _savedName = '';  
  String _savedEmail = '';  
  
  // --- ДОБАВИМ ФОКУС НОДЫ ---  
  
  // FocusNode для каждого поля, которым хотим управлять  
  final FocusNode _nameFocusNode = FocusNode();  
  final FocusNode _emailFocusNode = FocusNode();  
  
  @override  
  void dispose() {  
    _textController.dispose();  
  
    // !!! Обязательно очищайте FocusNode, когда виджет удаляется  
    _nameFocusNode.dispose();  
    _emailFocusNode.dispose();  
  
    super.dispose();  
  }  
  
  void _submitForm() {  
    if (_formKey.currentState!.validate()) {  
      _formKey.currentState!.save();  
  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(  
          content: Text('Всё ок! Имя: $_savedName, Email: $_savedEmail'),  
        ),  
      );  
      _formKey.currentState!.reset();  
      setState(() {  
        _savedName = '';  
        _savedEmail = '';  
      });  
    } else {  
      ScaffoldMessenger.of(context).showSnackBar(  
        SnackBar(content: Text('Пожалуйста, исправьте ошибки в форме')),  
      );  
    }  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    return ListView(  
      padding: const EdgeInsets.all(16.0),  
      children: [  
        Text(  
          "Форма с валидацией",  
          style: TextStyle(fontSize: 18),  
        ),  
        Form(  
          key: _formKey,  
          child: Column(  
            crossAxisAlignment: CrossAxisAlignment.start,  
            children: [  
            
              // ПОЛЕ ДЛЯ ИМЕНИ  
              TextFormField(  
                // --- ПРИВЯЗЫВАЕМ FocusNode к полю ---  
                focusNode: _nameFocusNode,  
  
                decoration: InputDecoration(  
                  labelText: 'Имя',  
                  hintText: 'Введите ваше имя',  
                  border: OutlineInputBorder(),  
                ),  
                keyboardType: TextInputType.name,  
                validator: (value) {  
                  if (value == null ||  
                      value.trim().isEmpty ||  
                      value.length < 2) {  
                    return 'Укажите имя размером не менее 2 символа';  
                  }  
                  return null;  
                },  
  
                onSaved: (newValue) {  
                  _savedName = newValue ?? '';  
                },  
  
                // --- ПЕРЕКЛЮЧЕНИЕ ФОКУСА при нажатии Готово на клавиатуре ---  
                onFieldSubmitted: (_) {  
                  // Переключить фокус на поле Email  
                  FocusScope.of(context).requestFocus(_emailFocusNode);  
                },  
              ),  
  
              SizedBox(height: 20),  
  
              // ПОЛЕ ДЛЯ EMAIL  
  
              TextFormField(  
                // --- ПРИВЯЗЫВАЕМ FocusNode к полю ---  
                focusNode: _emailFocusNode,  
  
                decoration: InputDecoration(  
                  labelText: 'Email',  
                  hintText: 'Введите ваш email',  
                  border: OutlineInputBorder(),  
                  prefixIcon: Icon(Icons.email), // Пример иконки в поле формы  
                ),  
                keyboardType:  
                    TextInputType.emailAddress, // Тип клавиатуры для Email  
                validator: (value) {  
                  if (value == null || value.isEmpty) {  
                    return 'Пожалуйста, введите ваш email';  
                  }  
                  if (!value.contains('@') || !value.contains('.')) {  
                    return 'Введите корректный email';  
                  }  
                  return null;  
                },  
                onSaved: (newValue) {  
                  _savedEmail = newValue ?? '';  
                },  
  
                // --- СКРЫТЬ КЛАВИАТУРУ при нажатии Готово на последнем поле ---  
                onFieldSubmitted: (_) {  
                  // Убрать фокус с поля - скрыть клавиатуру  
                  _emailFocusNode.unfocus();  
                  // сразу отправить форму  
                  _submitForm();  
                },  
              ),  
  
              SizedBox(height: 20),  
  
              // КНОПКА ДЛЯ ОТПРАВКИ ФОРМЫ  
              Center(  
                child: ElevatedButton(  
                  onPressed: _submitForm,  
                  child: Text('Отправить'),  
                ),  
              ),  
            ],  
          ),  
        ),  
      ],  
    );  
  }  
}

Важные моменты для работы с вводом и формами

Форматеры inputFormatters

Форматеры inputFormatters

Что, если нам нужно, чтобы пользователи вводили данные в определенном формате? Например:

Без контроля, пользователь может ввести что угодно, и тогда нам придется писать сложную логику проверки и очистки данных. К счастью, Flutter предоставляет мощный инструмент для этого — Форматеры ввода (Input Formatters)!

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

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

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

Сложность валидации: Вам придется писать много кода для проверки и очистки введенных данных.

Форматеры

TextInputFormatter — это абстрактный класс в пакете flutter/services.dart, который позволяет изменять вводимый пользователем текст в TextField или TextFormField. Он работает следующим образом: когда пользователь пытается ввести символ, форматер "перехватывает" это действие, обрабатывает символ и возвращает либо измененный текст, либо тот же текст, либо вообще запрещает ввод символа.

Для применения форматеров к текстовому полю, используется свойство inputFormatters в TextField или TextFormField, которое принимает список форматеров (List). Это значит, что вы можете применять несколько форматеров одновременно!

Форматер - Только числа
Светлая тема Темная тема
import 'package:flutter/material.dart';  
import 'package:flutter/services.dart'; // Важно! Для работы с форматерами  
  
class FormatterExample extends StatelessWidget {  
  const FormatterExample({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [  
            const Text('Только цифры (0-9):', style: TextStyle(fontSize: 16)),  
            TextField(  
              keyboardType: TextInputType.number, // Цифровая клавиатура  
              inputFormatters: [  
                // Позволяет вводить ТОЛЬКО цифры.  
                // Все остальные символы будут проигнорированы.                
                FilteringTextInputFormatter.digitsOnly,  
              ],  
              decoration: const InputDecoration(  
                hintText: 'Введите только цифры',  
                border: OutlineInputBorder(),  
              ),  
            ),  
          ],  
    );  
  }  
}

Форматер - Максимум 10 символов

Форматер - Максимум 10 символов
Светлая тема Темная тема
import 'package:flutter/material.dart';  
import 'package:flutter/services.dart'; // Важно! Для работы с форматерами  
  
  
class FormatterExample extends StatelessWidget {  
  const FormatterExample({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return Column(  
          crossAxisAlignment: CrossAxisAlignment.start,  
          children: [  
            const Text('Максимум 10 символов:', style: TextStyle(fontSize: 16)),  
            TextField(  
              inputFormatters: [  
                // LengthLimitingTextInputFormatter  
                // Ограничивает максимальное количество символов.                
                LengthLimitingTextInputFormatter(10),  
              ],  
              decoration: const InputDecoration(  
                hintText: 'Не более 10 символов',  
                border: OutlineInputBorder(),  
              ),  
            ),  
          ],  
    );  
  }  
}

Форматер - Только буквы

Форматер - Только буквы
Светлая тема Темная тема
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Важно! Для работы с форматерами

class FormatterExample extends StatelessWidget {
  const FormatterExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('Только буквы:', style: TextStyle(fontSize: 16)),
        TextField(
          inputFormatters: [
            // FilteringTextInputFormatter.allow с RegExp
            // Позволяет вводить символы, соответствующие регулярному выражению.
            FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z,А-Яа-я]')),
          ],
          decoration: const InputDecoration(
            hintText: 'Только буквы',
            border: OutlineInputBorder(),
          ),
        ),
      ],
    );
  }
}

Форматер - Пробелы запрещены

Форматер - Пробелы запрещены
Светлая тема Темная тема
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Важно! Для работы с форматерами

class FormatterExample extends StatelessWidget {
  const FormatterExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('Только буквы:', style: TextStyle(fontSize: 16)),
        TextField(
          inputFormatters: [
            // FilteringTextInputFormatter.allow с RegExp
            // Позволяет вводить символы, соответствующие регулярному выражению.
            FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z,А-Яа-я]')),
          ],
          decoration: const InputDecoration(
            hintText: 'Только буквы',
            border: OutlineInputBorder(),
          ),
        ),
      ],
    );
  }
}

Встроенные Форматеры – Готовые Решения

Flutter предоставляет несколько полезных встроенных форматеров:

Кастомные Форматеры

Что делать, если встроенных форматеров недостаточно? Например, для форматирования номера телефона с скобками и дефисами, или даты? Для этого мы можем создать свой собственный форматер, который будет наследовать от TextInputFormatter и переопределять метод formatEditUpdate.

Метод formatEditUpdate

Метод formatEditUpdate — это сердце вашего кастомного форматера. Он принимает два аргумента:

Ваша задача — вернуть новый объект TextEditingValue, который будет содержать отформатированный текст и правильное положение курсора.

TextEditingValue

TextEditingValue содержит:

Форматер номера телефона (XXX) XXX-XX-XX

Форматер, который автоматически добавляет скобки и дефисы к номеру телефона.

Кастомный форматер для номера телефона (XXX) XXX-XX-XX
Светлая тема Темная тема
import 'package:flutter/material.dart';  
import 'package:flutter/services.dart'; // Важно! Для работы с форматерами  
  
class FormatterExample extends StatelessWidget {  
  const FormatterExample({super.key});  
  
  @override  
  Widget build(BuildContext context) {  
    return Column(  
      crossAxisAlignment: CrossAxisAlignment.start,  
      children: [  
        const Text('Номер телефона в формате: (xxx) xxx-xx-xx'),  
        TextField(  
          inputFormatters: [  
            FilteringTextInputFormatter.digitsOnly,
            PhoneNumberFormatter(),  
          ],  
          decoration: const InputDecoration(  
            border: OutlineInputBorder(),  
          ),  
        ),  
      ],  
    );  
  }  
}  
  
// Кастомный форматер для номера телефона (XXX) XXX-XX-XX  
class PhoneNumberFormatter extends TextInputFormatter {  
  @override  
  TextEditingValue formatEditUpdate(  
      TextEditingValue oldValue,  
      TextEditingValue newValue,  
      ) {  
    final text = newValue.text;  
    final int newTextLength = text.length;  
  
    // Если ничего не введено или введен только бэкспейс  
    if (newTextLength == 0) {  
      return newValue.copyWith(text: '');  
    }  
  
    // Удаляем все нецифровые символы для обработки  
    String cleanedText = text.replaceAll(RegExp(r'[^\d]'), '');  
    int cleanedLength = cleanedText.length;  
  
    // Строим отформатированную строку  
    final StringBuffer newText = StringBuffer();  
    int selectionIndex = newValue.selection.end;  
  
    for (int i = 0; i < cleanedLength; i++) {  
      if (i == 0) {  
        newText.write('(');  
        // Сдвигаем курсор из-за '('
        if (selectionIndex == 0) selectionIndex++;   
      }  
      newText.write(cleanedText[i]);  
      // Сдвигаем курсор
      if (selectionIndex == i + 1 && i < 3) selectionIndex++;   
  
      if (i == 2) {  
        newText.write(') ');
        // Сдвигаем курсор из-за ') '
        if (selectionIndex == i + 1) selectionIndex += 2;   
      } else if (i == 5 || i == 7) {  
        newText.write('-');  
        // Сдвигаем курсор из-за '-'
        if (selectionIndex == i + 1) selectionIndex++;   
      }  
    }  
  
    // Ограничиваем ввод до 10 цифр (+форматирование)  
    if (cleanedLength > 10) {  
      cleanedText = cleanedText.substring(0, 10);  
      newText.clear();  
      for (int i = 0; i < cleanedText.length; i++) {  
        if (i == 0) newText.write('(');  
        newText.write(cleanedText[i]);  
        if (i == 2) newText.write(') ');  
        else if (i == 5 || i == 7) newText.write('-');  
      }  
    }  
  
    // Возвращаем новую TextEditingValue  
    return TextEditingValue(  
      text: newText.toString(),  
      selection: TextSelection.collapsed(offset: selectionIndex),  
    );  
  }  
}

Объяснение кастомного форматера PhoneNumberFormatter

Мы получаем oldValue и newValue. Сначала мы берем newValue.text и удаляем все нецифровые символы с помощью replaceAll(RegExp(r'[^\d]'), ''). Это важно, так как пользователь может вставить текст, который содержит не только цифры. Мы хотим работать только с чистыми цифрами.

Затем мы проходим по очищенному тексту и вставляем скобки и дефисы в нужных местах. Следим за положением курсора (selectionIndex): Это критически важно! Если мы вставляем символы ((, ), -, пробел), курсор должен сдвигаться вместе с ними, чтобы пользователь мог продолжать ввод интуитивно. Это самая сложная часть при написании кастомных форматеров.

В конце, мы создаем новый TextEditingValue с отформатированным текстом и правильным положением курсора.

Объединение Форматеров

Как вы могли заметить в примере с номером телефона, мы применили два форматера:

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