Когда вы создаете различные формы (например: регистрации или входа) на 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;
},
),
)
],
),
);
}
}
Логика работы следующая:
мы нажаем добавить новый пост
открывается окно с формой, вводим данные
если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.
Заключительный момент, добавим обработку результата в 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 запроса на сервер и обработка ошибок.
Полезные ссылки
Всем хорошего кода)
zim32
У меня с флатером пока не сложилось. Как раз пилю приложение. Сделал простой список через лист билдер. Использовал mobx. Ничего особо сложного, обычный список аля чат без картинок и всего прочего. Билдил в релизе. В итоге скрол подлагивает. Не сильно но нет ощущения нативного скрола. Все проверил, продебажил. Профайлер показывает что проблема в вызове Skia:flush, который выполняется иногда около 40мс что приводит к фреймдропам. В итоге запилил такой же список на котлине обычным рецайкл вью и стало хорошо. Разница словно флатеровский лист это 25 фпс а нативный 60
ookami_kb
Если это простой список, и он подлагивает в релизной сборке (не дебаг), то скорее всего, вы что-то сделали сильно неправильно.