Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.
Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.
И сегодня мы постараемся разобраться с этой темой на небольшом примере.
Ну что ж, погнали!
Наш план-
Часть 1- введение в разработку, первое приложение, понятие состояния;
-
Часть 2- файл pubspec.yaml и использование flutter в командной строке;
-
Часть 3- BottomNavigationBar и Navigator;
-
Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;
-
Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;
-
Часть 6 (текущая статья) - работа с формами, текстовые поля и создание поста.
-
Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;
-
Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;
-
Часть 9 - немного о тестировании;
Создание формы: добавление поста
Для начала добавим на нашу страницу HomePage
кнопку
по которой мы будем добавлять новый пост:
@overrideWidget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post List Page"), ), body: _buildContent(), // в первой части мы уже рассматривали FloatingActionButton floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { }, ), );}
Далее создадим новую страницу в файле
post_add_page.dart
:
import 'package:flutter/material.dart';class PostDetailPage extends StatefulWidget { @override _PostDetailPageState createState() => _PostDetailPageState();}class _PostDetailPageState extends State<PostDetailPage> { // TextEditingController'ы позволят нам получить текст из полей формы final TextEditingController titleController = TextEditingController(); final TextEditingController contentController = TextEditingController(); // _formKey пригодится нам для валидации final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post Add Page"), actions: [ // пункт меню в AppBar IconButton( icon: Icon(Icons.check), onPressed: () { // сначала запускаем валидацию формы if (_formKey.currentState!.validate()) { // здесь мы будем делать запроc на сервер } }, ) ], ), body: Padding( padding: EdgeInsets.all(15), child: _buildContent(), ), ); } Widget _buildContent() { // построение формы return Form( key: _formKey, // у нас будет два поля child: Column( children: [ // поля для ввода заголовка TextFormField( // указываем для поля границу, // иконку и подсказку (hint) decoration: InputDecoration( border: OutlineInputBorder(), prefixIcon: Icon(Icons.face), hintText: "Заголовок" ), // не забываем указать TextEditingController controller: titleController, // параметр validator - функция которая, // должна возвращать null при успешной проверки // или строку при неудачной validator: (value) { // здесь мы для наглядности добавили 2 проверки if (value == null || value.isEmpty) { return "Заголовок пустой"; } if (value.length < 3) { return "Заголовок должен быть не короче 3 символов"; } return null; }, ), // небольшой отступ между полями SizedBox(height: 10), // Expanded означает, что мы должны // расширить наше поле на все доступное пространство Expanded( child: TextFormField( // maxLines: null и expands: true // указаны для расширения поля на все доступное пространство maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Содержание", ), // не забываем указать TextEditingController controller: contentController, // также добавляем проверку поля validator: (value) { if (value == null || value.isEmpty) { return "Содержание пустое"; } return null; }, ), ) ], ), ); }}
Не забудьте добавить переход на страницу формы:
floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { Navigator.push(context, MaterialPageRoute( builder: (context) => PostDetailPage() )); },),
Запускаем и нажимаем на кнопку:

Вуаля! Форма работает.
Небольшая заметка
У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.
Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:
-
Flutter 2.0.6
-
Dart SDK version: 2.12.3
Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.
Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:
// ! указывает на то, что мы 100% уверены// что currentState не содержит null значение_formKey.currentState!.validate()
О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.
Мы задерживаться не будем и переходим к созданию POST запроса.
POST запрос для добавления данных на сервер
POST, как уже было отмечено, является одним из HTTP методов и служит для добавления новых данных на сервер.
Для начала добавим модель для нашего результата и изменим
немного класс Post
:
class Post { // все поля являются private // это сделано для инкапсуляции данных final int? _userId; final int? _id; final String? _title; final String? _body; // создаем getters для наших полей // дабы только мы могли читать их int? get userId => _userId; int? get id => _id; String? get title => _title; String? get body => _body; // добавим новый конструктор для поста Post(this._userId, this._id, this._title, this._body); // toJson() превращает Post в строку JSON String toJson() { return json.encode({ "title": _title, "content": _body }); } // Dart позволяет создавать конструкторы с разными именами // В данном случае Post.fromJson(json) - это конструктор // здесь мы принимаем объект поста и получаем его поля // обратите внимание, что dynamic переменная // может иметь разные типы: String, int, double и т.д. Post.fromJson(Map<String, dynamic> json) : this._userId = json["userId"], this._id = json["id"], this._title = json["title"], this._body = json["body"];}// у нас будут только два состоянияabstract class PostAdd {}// успешное добавлениеclass PostAddSuccess extends PostAdd {}// ошибкаclass PostAddFailure extends PostAdd {}
Затем создадим новый метод в нашем Repository
:
// добавление поста на серверFuture<PostAdd> addPost(Post post) async { final url = Uri.parse("$SERVER/posts"); // делаем POST запрос, в качестве тела // указываем JSON строку нового поста final response = await http.post(url, body: post.toJson()); // если пост был успешно добавлен if (response.statusCode == 201) { // говорим, что все ок return PostAddSuccess(); } else { // иначе ошибка return PostAddFailure(); }}
Далее добавим немного кода в PostController
:
// добавление поста// функция addPost будет принимать callback,// через который мы будет получать результатvoid addPost(Post post, void Function(PostAdd) callback) async { try { final result = await repo.addPost(post); // сервер вернул результат callback(result); } catch (error) { // произошла ошибка callback(PostAddFailure()); }}
Ну что ж пора нам вернуться к нашему представлению
PostAddPage
:
class PostDetailPage extends StatefulWidget { @override _PostDetailPageState createState() => _PostDetailPageState();}// не забываем поменять на StateMVCclass _PostDetailPageState extends StateMVC { // _controller может быть null PostController? _controller; // получаем PostController _PostDetailPageState() : super(PostController()) { _controller = controller as PostController; } // TextEditingController'ы позволят нам получить текст из полей формы final TextEditingController titleController = TextEditingController(); final TextEditingController contentController = TextEditingController(); // _formKey нужен для валидации формы final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post Add Page"), actions: [ // пункт меню в AppBar IconButton( icon: Icon(Icons.check), onPressed: () { // сначала запускаем валидацию формы if (_formKey.currentState!.validate()) { // создаем пост // получаем текст через TextEditingController'ы final post = Post( -1, -1, titleController.text, contentController.text ); // добавляем пост _controller!.addPost(post, (status) { if (status is PostAddSuccess) { // если все успешно то возвращаемя // на предыдущую страницу и возвращаем // результат Navigator.pop(context, status); } else { // в противном случае сообщаем об ошибке // SnackBar - всплывающее сообщение ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Произошла ошибка при добавлении поста")) ); } }); } }, ) ], ), body: Padding( padding: EdgeInsets.all(15), child: _buildContent(), ), ); } Widget _buildContent() { // построение формы return Form( key: _formKey, // у нас будет два поля child: Column( children: [ // поля для ввода заголовка TextFormField( // указываем для поля границу, // иконку и подсказку (hint) decoration: InputDecoration( border: OutlineInputBorder(), prefixIcon: Icon(Icons.face), hintText: "Заголовок" ), // указываем TextEditingController controller: titleController, // параметр validator - функция которая, // должна возвращать null при успешной проверки // и строку при неудачной validator: (value) { // здесь мы для наглядности добавили 2 проверки if (value == null || value.isEmpty) { return "Заголовок пустой"; } if (value.length < 3) { return "Заголовок должен быть не короче 3 символов"; } return null; }, ), // небольшой отступ между полями SizedBox(height: 10), // Expanded означает, что мы должны // расширить наше поле на все доступное пространство Expanded( child: TextFormField( // maxLines: null и expands: true // указаны для расширения поля maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Содержание", ), // указываем TextEditingController controller: contentController, // также добавляем проверку поля validator: (value) { if (value == null || value.isEmpty) { return "Содержание пустое"; } return null; }, ), ) ], ), ); }}
Логика работы следующая:
-
мы нажаем добавить новый пост
-
открывается окно с формой, вводим данные
-
если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.
Заключительный момент, добавим обработку результата в
PostListPage
:
floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // then возвращает объект Future // на который мы подписываемся и ждем результата Navigator.push(context, MaterialPageRoute( builder: (context) => PostDetailPage() )).then((value) { if (value is PostAddSuccess) { // SnackBar - всплывающее сообщение ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Пост был успешно добавлен")) ); } }); },),
Теперь тестируем:


К сожалению JSONPlaceholder на самом деле не добавляет пост и поэтому мы не сможем его увидеть среди прочих постов.
Заключение
Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.
Большая часть кода - это создание POST запроса на сервер и обработка ошибок.
Полезные ссылки
Всем хорошего кода)