Виджеты. Текстовые поля и Формы
Подготовка проекта
Сейчас освежим свои знания по вёрстке
Переходим по ссылке на GitFlic и скачиваем себе проект с роллами - https://gitflic.ru/project/igarett/course_flutter_sushi_app?branch=4.7-sushi-app-part-1

В прошлом курсе мы сделали:
- Гавный экран - список карточек с роллами
- Экран с подробным описанием товара
- Экран корзины
- Функционал добавления/удаления товара в корзине
Сейчас изучим более подробно про текстовые поля и создания форм во Flutter
Сделаем форму входа в наше приложение
Текстовые поля и Формы
- Открываем проект через
VS CodeилиAndroid Studio - В директории
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 — это базовый виджет для получения текстового ввода от пользователя. Он предоставляет множество опций для настройки внешнего вида, поведения и типа вводимых данных.

- Добавим виджет TextField
- Установим в него контроллер для управления
- Добавим кнопку, которая получает данные из TextField
- Получим количество символов и выведем сообщение с помощью SnackBar
Файл 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()
Этот виджет позволяет настроить:- Метки:
labelText(плавающая метка) илиhintText(подсказка внутри поля). - Иконки:
prefixIcon(слева) иsuffixIcon(справа). - Границы:
border(например,OutlineInputBorderилиUnderlineInputBorder). - Цвет фона:
filled: trueиfillColor. - Текст ошибки: Автоматически отображается под полем при валидации.
Файл 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 для оформления:
labelText,labelStyle: Плавающая метка, которая перемещается наверх при активации поля.hintText,hintStyle: Подсказка, видимая, когда поле пустое и не в фокусе (или когдаalignLabelWithHint: true).helperText,helperStyle: Дополнительный поясняющий текст под полем.errorText,errorStyle: Текст и стиль сообщения об ошибке. ВTextFormFieldобычно устанавливается автоматически результатомvalidator.icon: Иконка, расположенная перед полем, но вне его границы.prefixIcon,suffixIcon: Иконки внутри границы поля, слева и справа от текста соответственно. Их можно обернуть вInkWellилиIconButtonдля интерактивности (как в примере с кнопкой очистки).prefixText,prefixStyle,suffixText,suffixStyle: Не редактируемый текст перед или после основного содержимого поля.counterText,counterStyle,counter: Настройка отображения счетчика символов, часто используется сmaxLength.border,enabledBorder,focusedBorder,errorBorder,focusedErrorBorder,disabledBorder: Определяют внешний вид границы поля в различных состояниях (по умолчанию, активное, в фокусе, с ошибкой, неактивное). Вы можете использоватьOutlineInputBorderдля рамки илиUnderlineInputBorderдля подчеркивания.filled,fillColor: Включает и задает цвет фона поля.isDense,contentPadding: Управляют вертикальными отступами и внутренней плотностью содержимого поля.
Комплексный демонстрационный пример использования различных свойств 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(): Отправка формы и валидация:
- Когда пользователь пытается отправить форму (например, нажимает Enter в поле ввода), вызывается этот метод.
_formKey.currentState!.validate(): Это магический вызов! Он запускает проверку всех полей внутри нашей формы, которые имеютvalidator.- Если
validate()возвращаетtrue: Это значит, что все поля прошли проверку, ошибок нет. Мы показываем сообщение "Форма валидна!". - Если
validate()возвращаетfalse: Значит, есть ошибки. Flutter автоматически покажет сообщения об ошибках под соответствующими полями. Мы также покажем сообщение "Пожалуйста, исправьте ошибки в форме".
Построение интерфейса (build):
Formвиджет: Это "контейнер" для наших полей ввода. Именно к нему мы привязываем наш_formKey.TextFormField: Это само поле ввода.decoration: Здесь мы настраиваем внешний вид поля: метку (labelText), подсказку (hintText) и рамку (border).keyboardType: Указывает, какую клавиатуру показывать пользователю (в данном случае, для ввода имени).validator: Это функция, которая проверяет введённое значение.- Она получает текущий value (текст, введённый пользователем).
- Если value пустой, состоит только из пробелов или короче 2 символов, мы возвращаем текст ошибки.
- Если ошибок нет, возвращаем null. Если валидатор возвращает null, это означает, что поле валидно.
onFieldSubmitted: Это колбэк, который срабатывает, когда пользователь нажимает "Готово" или "Enter" на клавиатуре. Здесь мы вызываем наш метод_submitForm().
Добавим второе поле ввода
Теперь добавим второе поле ввода (для 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('Отправить'),
),
),
],
),
),
],
);
}
}
Важные моменты для работы с вводом и формами
- Используйте
TextFieldдля простых полей ввода, не требующих валидации или сохранения в рамкахForm. - Используйте
TextFormFieldдля полей ввода, являющихся частьюForm, так как он поддерживаетvalidatorиonSaved. - Настраивайте внешний вид полей с помощью объекта
InputDecoration - Для программного доступа к содержимому поля используйте
TextEditingController. Не забывайте очищать его вdispose(). - Реагируйте на пользовательский ввод с помощью колбэков
onChangedиonSubmitted(илиonFieldSubmittedдляTextFormField). - Виджет
Formгруппирует поля и предоставляет методы для централизованной валидации (validate()) и сохранения (save()) черезGlobalKey. - Определите правила валидации в колбэке
validatorкаждогоTextFormField. - Собирайте данные из полей в колбэке
onSavedкаждогоTextFormField. - Для управления фокусом между полями используйте
FocusNodeиFocusScope.of(context).requestFocus()/unfocus(). Не забывайте очищатьFocusNodeвdispose().
Форматеры inputFormatters
Форматеры inputFormatters
Что, если нам нужно, чтобы пользователи вводили данные в определенном формате? Например:
- Номер телефона: (XXX) XXX-XX-XX
- Дата: ДД.ММ.ГГГГ
- Номер кредитной карты: XXXX XXXX XXXX XXXX
- Только цифры: без букв и символов
- Только буквы: без цифр
Без контроля, пользователь может ввести что угодно, и тогда нам придется писать сложную логику проверки и очистки данных. К счастью, 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 предоставляет несколько полезных встроенных форматеров:
FilteringTextInputFormatter: Самый универсальный и часто используемый. Он может:FilteringTextInputFormatter.digitsOnly: Разрешает ввод только цифр (0-9).FilteringTextInputFormatter.allow(RegExp pattern): Разрешает ввод только тех символов, которые соответствуют заданному регулярному выражению.FilteringTextInputFormatter.deny(RegExp pattern): Запрещает ввод тех символов, которые соответствуют заданному регулярному выражению.LengthLimitingTextInputFormatter: Ограничивает максимальное количество символов, которые можно ввести в текстовое поле.
Кастомные Форматеры
Что делать, если встроенных форматеров недостаточно? Например, для форматирования номера телефона с скобками и дефисами, или даты? Для этого мы можем создать свой собственный форматер, который будет наследовать от TextInputFormatter и переопределять метод formatEditUpdate.
Метод formatEditUpdate
Метод formatEditUpdate — это сердце вашего кастомного форматера. Он принимает два аргумента:
- oldValue: Объект TextEditingValue, который представляет текст до изменения.
- newValue: Объект TextEditingValue, который представляет текст после попытки изменения пользователем.
Ваша задача — вернуть новый объект TextEditingValue, который будет содержать отформатированный текст и правильное положение курсора.
TextEditingValue
TextEditingValue содержит:
- text: Текущий текст в поле.
- selection: Объект TextSelection, который хранит положение курсора (baseOffset) и выделение (extentOffset).
- composing: Диапазон текста, который находится в процессе композиции (например, при вводе иероглифов).
Форматер номера телефона (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 с отформатированным текстом и правильным положением курсора.
Объединение Форматеров
Как вы могли заметить в примере с номером телефона, мы применили два форматера:
FilteringTextInputFormatter.digitsOnly: Это гарантирует, что пользователь может ввести только цифры.PhoneNumberFormatter(): Он уже берет эти цифры и форматирует их в нужный вид.
Форматеры в списке inputFormatters применяются последовательно. То есть, сначала текст проходит через первый форматер, затем результат его работы — через второй, и так далее. Это позволяет комбинировать их для достижения сложных эффектов.