Основы Flutter для начинающих (Часть VI)

от автора

Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.

Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.

И сегодня мы постараемся разобраться с этой темой на небольшом примере.

Ну что ж, погнали!

Наш план
  • Часть 1 — введение в разработку, первое приложение, понятие состояния;

  • Часть 2 — файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3 — BottomNavigationBar и Navigator;

  • Часть 4 — MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5 — http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 (текущая статья) — работа с формами, текстовые поля и создание поста.

  • Часть 7 — работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 — создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 — немного о тестировании;

Создание формы: добавление поста

Для начала добавим на нашу страницу HomePage кнопку по которой мы будем добавлять новый пост:

@override Widget 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(); }  // не забываем поменять на StateMVC class _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;               },             ),           )         ],       ),     );   }  }

Логика работы следующая:

  1. мы нажаем добавить новый пост

  2. открывается окно с формой, вводим данные

  3. если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.

Заключительный момент, добавим обработку результата в 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 запроса на сервер и обработка ошибок.

Полезные ссылки

Всем хорошего кода)

ссылка на оригинал статьи https://habr.com/ru/post/561174/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *