Вступление
Вы уже познакомились с некоторыми основными виджетами и способами их позиционировать. Сегодня вы познакомитесь с формами и кнопками.Это основные элементы интерфейса, с помощью которых пользователь может взаимодействовать с приложением: вводить данные или совершать действия. Поэтому важно создавать эти компоненты удобными и красивыми.
Текстовые поля
У Flutter есть два виджета для текстовых полей: TextField
и TextFormField
. Чаще всего используютTextField
.
Виджет TextField
имеет широкое API для кастомизации. Вы можете добавить label
, icon
, hint
и текст при ошибке, использовав класс InputDecoration
для свойства decoration
у TextField
.
Пример использования:
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter a search term',
),
),
Второй виджет для работы с текстом — TextFormField
. Он отличается тем, что его нужно использовать вместе с виджетом Form
. Это даёт возможность валидировать текстовые поля: проверять длину текста, вводимое содержимое и т. д.
TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Enter your username',
),
),
Как только вы создали свой TextField
, вам может пригодиться отслеживание введенного текста. Это нужно, к примеру, для валидации. Для таких целей нужно сделать три шага:
- Создать
TextEditingController
. - Передать контроллер внутрь
TextField
. - Достать данные из
TextEditingController
.
Шаг 1: создаём контроллер
Первым делом давайте создадим TextEditingController
. Это класс, который позволяет взаимодействовать с виджетом TextField
. С его помощью можно:
- Установить первоначальный текст для
TextField
. - Получить текущее значения текстового поля.
- И много чего ещё.
Давайте создадим контроллер внутри StatefullWidget
. Это позволит при вызове dispose()
метода у State
очистить выделенные ресурсы. Это важный момент, о котором стоит всегда помнить.
class _MyCustomFormState extends State<MyCustomForm> {
final myController = TextEditingController();
@override
void dispose() {
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
/// создание TextField
}
}
Шаг 2: передать контроллер
Вторым действием мы должны передать наш TextEditingController
внутрь TextField
return TextField(
controller: myController,
);
Как только мы передали myController
, у нас появилась возможность для отслеживания актуального состояния ввода TextField
.
Шаг 3: достать введённый текст
Для того чтобы получить текущее введённое значение в текстовом поле, можно использовать геттер text
.
Полный пример использования TextField
может быть следующим:
В некоторых случаях бывает полезно не просто получать актуальное состояние в момент, когда это потребуется, но и отслеживать в режиме реального времени всё, что происходит с TextField
.
Например, вы создаёте приложение для поиска товаров и хотите при вводе очередного символа показывать пользователю подсказки для ввода.
У нас есть два способа добиться этого:
- Использовать параметр
onChanged()
уTextField
илиTextFormField
. - Использовать уже знакомый нам
TextEditingController
.
Давайте начнём разбор с первого пункта — это самый простой вариант.
У TextField
или TextFormField
есть параметр onChanged
, который принимает анонимную функцию. Она вызывается каждый раз после изменения текста. Пример:
TextField(
onChanged: (text) {
print('Введенный текст: $text');
},
),
Более продвинутый, но более сложный подход — использовать TextEditingController
. Для этого мы должны сделать следующее:
- Создать
TextEditingController
. - Передать его в
TextField
. - Создать функцию, которая будет вызываться при каждом изменении введённых данных.
- Передать её в метод
addListener()
.
С шагами 1 и 2 вы познакомились ранее. Разберём остальные.
Метод addListener()
принимает функцию типа VoidCallback
, поэтому для отслеживания ввода можно использовать такой вариант:
void _printLatestValue() {
print('Последний введенный текст: ${myController.text}');
}
Эта функция не имеет параметров с актуальным значением текстового поля. Получить текст возможно, используя свойство text
.
Чтобы связать нашу функцию и TextEditingController
, нужно передать её в метод addListener()
.
Сделать это можно, например, в initState()
. Важно: необходимо иметь ссылку на созданные TextEditingController
для того чтобы в будущем вызвать метод dispose()
и высвободить неиспользуемые ресурсы.
Итого, полный пример:
Форматтеры
Введённый текст можно форматировать. Представим, что у нас есть поле для CVV-кода банковской карты. Мы хотим, чтобы пользователь мог ввести только три символа и только цифры: буквы и знаки препинания нам не подходят.
Для таких целей можно использовать параметр inputFormatters
у виджета TextField
. Мы передаём в него список из TextInputFormatter
, которые запускаются по порядку и форматируют текст. TextInputFormatter
— класс, который позволяет провалидировать и отформатировать введенный текст.
Вот что нужно сделать, чтобы добиться желаемого:
- Создать
FilteringTextInputFormatter
, который бы валидировал, что на ввод подаются только цифры. - Создать
LengthLimitingTextInputFormatter
, который бы валидировал длину введенной строки. - Использовать подходящий тип клавиатуры TextInputType чтобы пользователю комфортнее вводить CVV-код.
Для начала создадим FilteringTextInputFormatter
. FilteringTextInputFormatter
— это подкласс TextInputFormatter
, он обладает удобным интерфейсом для наших целей. Используем именованный конструктор .allow()
, в него мы передаём паттерн, который будет валидировать строку.
FilteringTextInputFormatter.allow(
RegExp(r'[0-9]'),
);
В качестве паттерна мы будем использовать класс RegExp
для работы с регулярными выражениями. В итоге мы получим форматер, который позволяет вводить только цифры.
LengthLimitingTextInputFormatter
— это подкласс TextInputFormatter
. Он принимает параметр maxLength
, который ограничивает введённый текст:
LengthLimitingTextInputFormatter(3)
Также для удобства пользователя давайте используем параметр keyboardType
у TextField
. В него мы передаём желаемый вид клавиатуры, который будет открываться при тапе на наше поле. В данном случае нам подойдёт TextInputType.number
.
Теперь давайте объединим форматеры и keyboardType
и посмотрим на итоговый результат:
class _MyCustomFormState extends State<MyCustomForm> {
@override
Widget build(BuildContext context) {
final numberFormatter = FilteringTextInputFormatter.allow(
RegExp(r'[0-9]'),
);
final lengthFormatter = LengthLimitingTextInputFormatter(3);
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
keyboardType: TextInputType.number,
inputFormatters: [numberFormatter, lengthFormatter],
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'cvv',
),
),
),
),
);
}
}
Фокус
Важная часть при работе с текстовыми полями — фокус.
Будем считать, что, когда текстовое поле выбрано и пользователь что-то набирает в нём, данное поле находится «в фокусе». Мы можем управлять тем, какое поле сейчас находится в фокусе и по каким правилам фокус переходит на следующее поле.
Это важный момент при создании интуитивно понятных форм. Например, у вас есть экран с поисковой строкой. Было бы здорово, чтобы при открытии фокус уже был на текстовом поле — и пользователь сразу начал бы набирать свой запрос.
Это не только может повысить метрики для вашего приложения по количеству поисковых запросов, но и сделает опыт пользования приложением (UX) более приятным.
Чтобы добиться такого поведения, необходимо использовать свойство autofocus
.
TextField(
autofocus: true,
);
Но бывают ситуации, когда нам нужно управлять фокусом и назначать его на TextField
при определённых условиях или перемещать его от одного TextField
к другому. В этом случае нам поможет FocusNode
. FocusNode
— класс, который позволяет управлять состоянием фокуса текстовых полей. Нужно сделать три шага:
- Создать
FocusNode
. - Передать
FocusNode
вTextField
. - Использовать
FocusNode
для назначения фокусаTextField
.
Прежде всего давайте создадим FocusNode
. Подробнее про механизм работы Flutter с фокусами можно почитать в официальной документации.
Также как и TextEditingController
, при работе с FocusNode
мы должны не забывать, что это долгоживущая сущность и необходимо очищать связанные с ней данные — например, в методе dispose()
, как мы делали это для контроллера TextEditingController
.
Создать FocusNode
можно так:
class _MyCustomFormState extends State<MyCustomForm> {
late FocusNode myFocusNode;
@override
void initState() {
super.initState();
myFocusNode = FocusNode();
}
@override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
}
}
После этого нам необходимо передать его в наш TextField
:
@override
Widget build(BuildContext context) {
return TextField(
focusNode: myFocusNode,
);
}
Ну и в самом конце мы можем вызвать фокус для данного TextField
, используя метод requestFocus()
у нашей myFocusNode
:
FloatingActionButton(
onPressed: () => myFocusNode.requestFocus(),
),
Полный пример использования:
Формы
Часто необходимо провалидировать корректность введённых данных: чтобы в пароле были и цифры, в номере телефона не было букв и т. д. Для это нужно сделать три простых шага:
- Создать виджет
Form
c ключомGlobalKey
. - Добавить
TextFormField
c логикой для валидации. - Связать всё вместе.
Form
— это контейнер, в котором будут храниться наши текстовые поля. Мы должны передать GlobalKey
, чтобы к нему было удобно обращаться.
class CustomForm extends StatefulWidget {
const CustomForm({super.key});
@override
CustomFormState createState() {
return CustomFormState();
}
}
class CustomFormState extends State<CustomForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Container(),
);
}
}
Затем следует создать наше текстовое поле при помощи виджета TextFormField
. Данный виджет внутри использует TextField
, но имеет дополнительные возможности. В отличие от TextField,
у него есть возможность валидировать данные — благодаря параметру validator
. Данный параметр принимает на вход метод с одним параметром, который обозначает текущий введённый текст.
TextFormField(
validator: (value) {
if (value == null || value.length < 10) {
return 'Ненадежный пароль';
}
return null;
},
);
В данном примере мы проверяем пароль на надёжность.
Если validator
вернёт что угодно, кроме null
, то поле не прошло валидацию. Если возвращается строка, то TextFormField
сам сообщит пользователю о проблеме.
Чтобы провалидировать данные, мы должны получить состояние виджета Form
и вызвать у него метод validate()
.
TextButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// прошли валидацию
}
// не прошли валидацию
},
child: Text('Валидировать'),
);
Если теперь соединить всё вместе, то получим форму, в которую можно ввести данные и провалидировать их. А в зависимости от результата валидации либо сообщить пользователю, где он ошибся, либо отправить данные на сервер.
class CustomForm extends StatefulWidget {
const CustomForm({super.key});
@override
CustomFormState createState() {
return CustomFormState();
}
}
class CustomFormState extends State<CustomForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
validator: (value) {
if (value == null || value.length < 10) {
return 'Ненадежный пароль';
}
return null;
},
),
TextButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// Прошли валидацию
}
// Не прошли валидацию
},
child: Text('Валидировать'),
),
],
),
),
);
}
}
Как избежать перекрытия контента клавиатурой
Страницы с текстовыми полями зачастую устроены стандартным образом: какой-то заголовок, по центру текстовые поля, а в самом низу располагается кнопка подтверждения ввода.
При такой конфигурации элементов часто бывают проблемы с тем, что клавиатура перекрывает кнопку подтверждения, и пользователю приходится её принудительно закрывать, чтобы подтвердить заполнение формы.
Чтобы решить эту проблему, можно поместить контент в какую-то скроллющуюся обёртку. Это могут быть виджеты ListView
или SingleChildScrollView
.
Ниже — результат применения ListView. Теперь у пользователя есть возможность подскролливать контент на экране.
ListView(
children: [
Placeholder(fallbackHeight: 400),
TextField(),
Placeholder(fallbackHeight: 400),
],
),
Кнопки
Во Flutter уже есть много готовых кнопок, вот некоторые из них:
ElevatedButton
— стилизована так, будто бы парит в воздухе.IconButton
— отлично подходит, когда нужно, чтобы по нажатию на иконку происходило какое-то действие.OutlineButton
— кнопка с дополнительной обводкой вокруг.TextButton
— привычная нам кнопка-текст.
Многие из них используют под капотом виджет RawMaterialButton
как основу и дополняют его своими свойствами. RawMaterialButton
представляет собой основу для кнопок в стиле Material. Подробнее про стиль можно почитать на официальном сайте — https://m3.material.io/components/all-buttons.
Кроме обычных кнопок в интерфейсе стоит отметить ещё FloatingActionButton
. Она обычно используется как главное действие, которое пользователь должен совершить на экране. Зачастую её кладут в Scaffold
:
Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.send),
),
)
Radio
Этот тип кнопок отлично подходит, когда нужно предоставить пользователю выбор между взаимоисключающими вариантами. Во Flutter для этого используется виджет Radio.
Чтобы создать данный виджет, необходимо передать следующие параметры:
value
— значение, с которым будет сравниватьсяgroupValue
, и если сравнение пройдёт успешно, то данная кнопка будет выбрана.groupValue
— общее значение для несколькихRadio Button
. Мы как раз управляем состоянием нескольких кнопок, меняя его.onChanged
— в этом колбэке мы получаем актуальноеvalue
для кнопки. Его стоит использовать для обновленияgroupValue
и перерисовки компонента.
Radio<String>(
value: 'Каша',
groupValue: breakfast,
onChanged: (String? value) {
setState(() {
breakfast = value;
});
},
),
Пример, в котором у нас есть несколько таких кнопок:
class _RadioButtonExampleState extends State<RadioButtonExample> {
String? breakfast = 'Каша';
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
title: const Text('Каша'),
leading: Radio<String>(
value: 'Каша',
groupValue: breakfast,
onChanged: (String? value) {
setState(() {
breakfast = value;
});
},
),
),
ListTile(
title: const Text('Творог'),
leading: Radio<String>(
value: 'Творог',
groupValue: breakfast,
onChanged: (String? value) {
setState(() {
breakfast = value;
});
},
),
),
],
);
}
}
Checkbox
Этот элемент бывает очень полезным, когда мы хотим дать пользователю выбрать несколько вещей из какого-то списка или когда нам нужен выключатель для управления какой-то настройкой.
Checkbox(
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value;
});
},
);
По дефолту данный переключатель может находиться в двух состояниях: включен или выключен. Но можно задать свойство tristate: true
, и тогда мы получим промежуточное состояние.
Жесты
Но иногда бывают случаи, когда мы хотим, чтобы действие происходило не по нажатии на кнопку, а на какой-то иной объект или произвольную область на экране. Для этих целей обычно используют виджеты GestureDetector
или InkWell
.
Сначала рассмотрим InkWell
. Он служит для обработки «простых» жестов, таких как тапы, двойные тапы и некоторые другие:
InkWell(
onTap: () {
print("onTap");
},
child: FlutterLogo(
size: 200,
),
);
При нажатии он добавляет поверхности эффект, будто бы это кнопка. Если такой визуальный эффект не нужен и/или нужно обрабатывать более сложные жесты: слайды в разные стороны, сильные / слабые нажатия и другое, то стоит взглянуть в сторону виджета GestureDetector
.
Чтобы им воспользоваться, необходимо обернуть им искомый виджет.
GestureDetector(
onTap: () {
print("onTap");
},
onHorizontalDragStart: ((details) {
print("onHorizontalDragStart");
}),
child: FlutterLogo(
size: 200,
),
);
В этом примере кроме обычных нажатий мы можем отслеживать, в какой момент пользователь начал использовать горизонтальный слайд на нашем виджете.
Вывод
Flutter имеет богатую библиотеку уже готовых компонентов и классов. Это позволяет создавать красивые и удобные приложения. Кроме того, вы можете создать свои виджеты, которые обрабатывают ввод, жесты и много чего ещё. Важно помнить о конечных пользователях вашего приложения, об их удобстве и удовольствии.