Lesson 4

Урок 4. Форма ввода, проверка правильного ввода

Егор Долгов · 05.07.2019
This article was written for an older Flutter version and is being updated. Some code may not compile on the current version.

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

Перед использованием внесенных данных важно проверить их после ввода:

  • внесены ли данные в обязательные поля;
  • соответствует ли содержимое значению поля (например, в поле возраст должно быть целочисленное число);
  • выбрано значение в группе переключателей;
  • и т.д.

1. class Form()

Главным контейнером для формы является виджет-класс Form , он позволяет объединить в себе поля ввода. Обращаясь к состоянию формы FormState , можно проверить корректное заполнение полей, сбросить значения по умолчанию и сохранить значения.

Создадим виджет-класс MyForm от супер-класса StatefulWidget , который при построении будет возвращать виджет Form . Такая конструкция позволит перерисовать изменения в форме с помощью setState((){}) , например: нажатие на чекбокс или радиокнопку сменит их состояние, но не изменит их внешнего вида, пока не будут отрисованы заново.

import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyFormState();
}

class MyFormState extends State {
  final _formKey = GlobalKey<FormState>();

  Widget build(BuildContext context) {
    return new Form(key: _formKey, child: new Column());
  }
}

void main() => runApp(
    new MaterialApp(
        debugShowCheckedModeBanner: false,
        home: new Scaffold(
            appBar: new AppBar(title: new Text('Flutter.su - Форма ввода')),
            body: new MyForm()
        )
    )
);

Мы указали параметр key со значение formKey – данная константа позволит нам обращаться из дочерних элементов к функционалу формы для проверки данных, сохранения или сброса значений.

2. class TextFormField() – текстовое поле ввода

Добавим в форму текстовое поле ввода, заголовок перед ним и кнопку для инициализации проверки данных.

import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyFormState();
}

class MyFormState extends State {
  final _formKey = GlobalKey<FormState>();

  Widget build(BuildContext context) {
    return Container(padding: EdgeInsets.all(10.0), child: new Form(key: _formKey, child: new Column(children: <Widget>[
      new Text('Имя пользователя:', style: TextStyle(fontSize: 20.0),),
      new TextFormField(validator: (value){
        if (value.isEmpty) return 'Пожалуйста введите свое имя';
      }),

      new SizedBox(height: 20.0),

      new RaisedButton(onPressed: (){
        if(_formKey.currentState.validate()) Scaffold.of(context).showSnackBar(SnackBar(content: Text('Форма успешно заполнена'), backgroundColor: Colors.green,));
      }, child: Text('Проверить'), color: Colors.blue, textColor: Colors.white,),
    ],)));
  }
}

void main() => runApp(
    new MaterialApp(
        debugShowCheckedModeBanner: false,
        home: new Scaffold(
            appBar: new AppBar(title: new Text('Flutter.su - Форма ввода')),
            body: new MyForm()
        )
    )
);
  • return Container(padding: EdgeInsets.all(10.0), child: new Form(… – виджет формы мы обернули контейнером с отступами.
  • new Text('Имя пользователя:', style: TextStyle(fontSize: 20.0),) – добавили заголовок перед полем ввода
  • new TextFormField( – виджет текстового поля ввода
  • validator: (value){}() – эту функцию будет инициализировать виджет Form, когда мы обратимся к ней для проверки данных formKey.currentState.validate()
  • validator: (value){if (value.isEmpty) return 'Пожалуйста введите свое имя';} – проверяем значение в форме, если оно пустое isEmpty , значит возвращаем строку-ошибку.
  • new SizedBox(height: 20.0) – создаем контейнер для визуального разделения виджетов между собой по вертикали
  • new RaisedButton(onPressed: (){ – создаем виджет кнопки, в предыдущих уроках мы использовали FlatButton (это непринципиально)
  • if( formKey.currentState.validate()) – проверяем, проходят ли проверку поля ввода, если у полей ввода есть функции проверки в параметре validator . Чтобы formKey.currentState.validate() вернул значение true == в полях нет ошибок, все функции должны вернуть значение null (т.е. ничего не возвращать или return null).

Если какое-то поле вернуло ошибку, то под полем появится сообщение об ошибке. Если все поля прошли проверку, мы вызываем: Scaffold.of(context).showSnackBar(SnackBar(content: Text('Форма успешно заполнена'), backgroundColor: Colors.green,)); – всплывающее уведомление внизу экрана.

class TextFormField() – текстовое поле ввода
class TextFormField() – текстовое поле ввода

3. Проверка поля E-mail

Текстовое значение электронного адреса должно быть правильно набрано если мы хотим потом связаться по нему с человеком. Самая простая проверка которую можно придумать – это проверить есть ли символ @ в строке. Для этого можно было бы использовать такую проверку:

validator: (value){
  if (value.isEmpty) return 'Пожалуйста введите свой E-mail';
  if (!value.contains('@')) return 'Это не E-mail';
}

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

import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyFormState();
}

class MyFormState extends State{
  final _formKey = GlobalKey<FormState>();

  Widget build(BuildContext context) {
    return Container(padding: EdgeInsets.all(10.0), child: new Form(key: _formKey, child: new Column(children: <Widget>[
      new Text('Имя пользователя:', style: TextStyle(fontSize: 20.0),),
      new TextFormField(validator: (value){
        if (value.isEmpty) return 'Пожалуйста введите свое имя';
      }),

      new SizedBox(height: 20.0),

      new Text('Контактный E-mail:', style: TextStyle(fontSize: 20.0),),
      new TextFormField(validator: (value){
        if (value.isEmpty) return 'Пожалуйста введите свой Email';

        String p = "[a-zA-Z0-9+.\_\%-+]{1,256}@[a-zA-Z0-9][a-zA-Z0-9-]{0,64}(.[a-zA-Z0-9][a-zA-Z0-9-]{0,25})+";
        RegExp regExp = new RegExp(p);

        if (regExp.hasMatch(value)) return null;

        return 'Это не E-mail';
      }),

      new SizedBox(height: 20.0),

      new RaisedButton(onPressed: (){
        if(_formKey.currentState.validate()) Scaffold.of(context).showSnackBar(SnackBar(content: Text('Форма успешно заполнена'), backgroundColor: Colors.green,));
      }, child: Text('Проверить'), color: Colors.blue, textColor: Colors.white,),
    ],)));
  }
}

void main() => runApp(
    new MaterialApp(
        debugShowCheckedModeBanner: false,
        home: new Scaffold(
            appBar: new AppBar(title: new Text('Flutter.su - Форма ввода')),
            body: new MyForm()
        )
    )
);

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

Если пакет validator не устанавливается, смотрите заметку: Возможные проблемы при обновлении до версии Flutter Release Preview 2 и Dart SDK 2.1.0

Есть модуль написанный на дарт «validator» pub.dartlang.org/packages/validator , если его подключить, то можно проверять множество типов, с его помощью можно проверить и e-mail простой функцией isEmail(String str) которая скрывает от нас реализацию.

class TextFormField() – проверка e-mail
class TextFormField() – проверка e-mail

4. class RadioListTile() – переключатель (радиокнопка)

Переключатели объединяются в группу по параметру, который они изменяют. Так, например, добавим переключатели к нашей форме с выбором мужского или женского пола.

Можно определить переменную gender , для группировки переключателей, в нашем виджете с целочисленным значением, где 0 – мужской пол, а 1 – женский пол:

class MyFormState extends State{
  final _formKey = GlobalKey

тогда сами переключатели имели бы такой вид:

new Text('Ваш пол:', style: TextStyle(fontSize: 20.0),),

new RadioListTile(
  title: const Text('Мужской'),
  value: 0,
  groupValue: _gender,
  onChanged: (int value) {setState(() { _gender = value;});},
),

new RadioListTile(
  title: const Text('Женский'),
  value: 1,
  groupValue: _gender,
  onChanged: (int value) {setState(() { _gender = value;});},
),

new SizedBox(height: 20.0),
//…
  • groupValue: gender – группировка переключателей по значению, а также значение по умолчанию. Если бы в классе MyFormState мы объявили int gender = 0 – то по умолчанию был бы выбран мужской пол.
  • onChanged: (int value) {setState(() { gender = value;});} – когда мы нажимаем на переключатель и меняем его состояние, происходит событие в котором мы меняем значение на новое и запрашиваем перестроение нашего виджета MyFormState с помощью setState((){});

В полной реализации мы решили отойти от int значений для выбора пола и создали перечисление GenderList с помощью типа enum . Для этого в файле main.dart пишем:

import 'package:flutter/material.dart';

enum GenderList {male, female}
//…

Теперь там где мы использовали int – меняем на GenderList и значения 0 на GenderList.male и 1 на GenderList.female :

GenderList _gender;
//…
new Text('Ваш пол:', style: TextStyle(fontSize: 20.0),),

new RadioListTile(
  title: const Text('Мужской'),
  value: GenderList.male,
  groupValue: _gender,
  onChanged: (GenderList value) {setState(() { _gender = value;});},
),

new RadioListTile(
  title: const Text('Женский'),
  value: GenderList.female,
  groupValue: _gender,
  onChanged: (GenderList value) {setState(() { _gender = value;});},
),

new SizedBox(height: 20.0),

5. class CheckboxListTile() – флажок (чекбокс)

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

Добавим переменную «согласия» agreement типа bool в наш класс:

class MyFormState extends State{
  final _formKey = GlobalKey<FormState>();
  GenderList _gender;
  bool _agreement = false;
  • true – флажок установлен, пользователь согласен
  • false – флажок снят, пользователь не принял соглашение.

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

new SizedBox(height: 20.0),

new CheckboxListTile(
  value: _agreement,
  title: new Text('Я ознакомлен'+(_gender==null?'(а)':_gender==GenderList.male?'':'а')+' с документом "Согласие на обработку персональных данных" и даю согласие на обработку моих персональных данных в соответствии с требованиями "Федерального закона О персональных данных № 152-ФЗ".'),
  onChanged: (bool value) => setState(() => _agreement = value)
),

6. Проверка class RadioListTile() и class CheckboxListTile()

Проверку текстовых полей при нажатии на кнопку «Проверить» мы проводили так:

new RaisedButton(onPressed: (){
  if(_formKey.currentState.validate())

И если функция в значения validator в текстовых полях не возвращала ошибок, мы выводили сообщение об успешном заполнении формы:

Scaffold.of(context).showSnackBar(SnackBar(content: Text('Форма успешно заполнена'), backgroundColor: Colors.green,)

Но у виджетов RadioListTile и CheckboxListTile нет параметра validator для функции проверки.

Поэтому мы должны встроить проверку значений переключателей и флажков после или до основной проверки форм if( formKey.currentState.validate())

new RaisedButton(onPressed: (){
  if(_formKey.currentState.validate()) {
    Color color = Colors.red;
    String text;

    if (_gender == null) text = 'Выберите свой пол';
    else if (_agreement == false) text = 'Необходимо принять условия соглашения';
    else {text = 'Форма успешно заполнена'; color = Colors.green;}

    Scaffold.of(context).showSnackBar(SnackBar(content: Text(text), backgroundColor: color,));
  }
}, child: Text('Проверить'), color: Colors.blue, textColor: Colors.white,),
Проверка class RadioListTile() и class CheckboxListTile()
Проверка class RadioListTile() и class CheckboxListTile()

7. Полный листинг программы форма с проверкой

import 'package:flutter/material.dart';

enum GenderList {male, female}

class MyForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyFormState();
}

class MyFormState extends State {
  final _formKey = GlobalKey<FormState>();
  GenderList _gender;
  bool _agreement = false;

  Widget build(BuildContext context) {
    return Container(padding: EdgeInsets.all(10.0), child: new Form(key: _formKey, child: new Column(children: <Widget>[
      new Text('Имя пользователя:', style: TextStyle(fontSize: 20.0),),
      new TextFormField(validator: (value){
        if (value.isEmpty) return 'Пожалуйста введите свое имя';
      }),

      new SizedBox(height: 20.0),

      new Text('Контактный E-mail:', style: TextStyle(fontSize: 20.0),),
      new TextFormField(validator: (value){
        if (value.isEmpty) return 'Пожалуйста введите свой Email';

        String p = "[a-zA-Z0-9+.\_\%-+]{1,256}@[a-zA-Z0-9][a-zA-Z0-9-]{0,64}(.[a-zA-Z0-9][a-zA-Z0-9-]{0,25})+";
        RegExp regExp = new RegExp(p);

        if (regExp.hasMatch(value)) return null;

        return 'Это не E-mail';
      }),

      new SizedBox(height: 20.0),

      new Text('Ваш пол:', style: TextStyle(fontSize: 20.0),),

      new RadioListTile(
        title: const Text('Мужской'),
        value: GenderList.male,
        groupValue: _gender,
        onChanged: (GenderList value) {setState(() { _gender = value;});},
      ),

      new RadioListTile(
        title: const Text('Женский'),
        value: GenderList.female,
        groupValue: _gender,
        onChanged: (GenderList value) {setState(() { _gender = value;});},
      ),

      new SizedBox(height: 20.0),

      new CheckboxListTile(
        value: _agreement,
        title: new Text('Я ознакомлен'+(_gender==null?'(а)':_gender==GenderList.male?'':'а')+' с документом "Согласие на обработку персональных данных" и даю согласие на обработку моих персональных данных в соответствии с требованиями "Федерального закона О персональных данных № 152-ФЗ".'),
        onChanged: (bool value) => setState(() => _agreement = value)
      ),

      new SizedBox(height: 20.0),

      new RaisedButton(onPressed: (){
        if(_formKey.currentState.validate()) {
          Color color = Colors.red;
          String text;

          if (_gender == null) text = 'Выберите свой пол';
          else if (_agreement == false) text = 'Необходимо принять условия соглашения';
          else {text = 'Форма успешно заполнена'; color = Colors.green;}

          Scaffold.of(context).showSnackBar(SnackBar(content: Text(text), backgroundColor: color,));
        }
      }, child: Text('Проверить'), color: Colors.blue, textColor: Colors.white,),
    ],)));
  }
}

void main() => runApp(
    new MaterialApp(
        debugShowCheckedModeBanner: false,
        home: new Scaffold(
            appBar: new AppBar(title: new Text('Flutter.su - Форма ввода')),
            body: new MyForm()
        )
    )
);

8. Задание для практики: калькулятор площади

Задание для практики: калькулятор площади
Задание для практики: калькулятор площади

Ниже будет приведен листинг кода калькулятора площади, его вы можете использовать для задания или написать свой.

Задание:

  1. Добавить переключатели между единицами измерения: мм, см, м и т.д.
  2. Задать значения по умолчанию.
  3. Изменить тип int на double

Для выполнения задания вам понадобится параметр initialValue: 'String' в классе TextFormField – задает значение текстового поля по умолчанию.

import 'package:flutter/material.dart';

class MyCalulateArea extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyCalulateAreaState();
}

class MyCalulateAreaState extends State<MyCalulateArea> {
  final _formKey = GlobalKey<FormState>();
  int _width;
  int _height;
  int _area;

  Widget build(BuildContext context) {
    return new Form(key: _formKey, child: new Column(
        children: [
          new Row(children: <Widget>[
            new Container(padding:EdgeInsets.all(10.0),child: new Text('Ширина (мм):')),
            new Expanded(child: Container(padding:EdgeInsets.all(10.0),child:
            new TextFormField(validator: (value){
              if (value.isEmpty) return 'Задайте Ширину';

              try {
                _width = int.parse(value);
              } catch(e) {
                _width = null;
                return e.toString();
              }
            })
            )),
          ]
          ),

          new SizedBox(height: 10.0),

          new Row(children: <Widget>[
            new Container(padding:EdgeInsets.all(10.0),child: new Text('Высота (мм):')),
            new Expanded(child: Container(padding:EdgeInsets.all(10.0),child:
            new TextFormField(validator: (value){
              if (value.isEmpty) return 'Задайте Высоту';

              try {
                _height = int.parse(value);
              } catch(e) {
                _height = null;
                return e.toString();
              }
            })
            )),
          ]
          ),

          new SizedBox(height: 10.0),

          new RaisedButton(onPressed: (){
            if(_formKey.currentState.validate()) {
              setState(() {
                if (_width is int && _height is int) _area = _width * _height;
              });
            }
          }, child: Text('Вычислить'), color: Colors.blue, textColor: Colors.white,),

          new SizedBox(height: 50.0),

          new Text(_area == null ? 'задайте параметры' : 'S = $_width * $_height = ${_area} (мм2)', style: TextStyle(fontSize: 30.0),)
        ]
    ));
  }
}

void main() => runApp(
    new MaterialApp(
        debugShowCheckedModeBanner: false,
        home: new Scaffold(
            appBar: new AppBar(title: new Text('Калькулятор площади')),
            body: new MyCalulateArea()
        )
    )
);

Ваши вопросы и ответы в виде кода программы присылайте на нашу почту [contact@flutter.ru](mailto:contact@flutter.ru?subject=Урок № 4), в теме письма обязательно указывайте «Урок № 4».