Разработка приложения Sushi App. Часть 2
Форма авторизации
Продолжаем дорабатывать приложение по заказу роллов и суши. Сейчас мы создадим один из самых важных экранов любого приложения – экран входа (или авторизации)
Что сделаем и чему научимся:
- Создадим новый экран
login_screen.dart - Разместим на нем поля для ввода логина и пароля, а также кнопки "Войти" и "Зарегистрироваться".
- Научимся "читать" данные из полей ввода.
- Добавим проверку (валидацию): если поля пустые или данные неверны, пользователь не сможет войти.
- При успешном входе – перенаправим пользователя на главный экран приложения.
- Добавим в папку screens новый файл
login_screen.dart - Добавим в него
StatefulWidgetи назовёмLoginScreenэто будет экран авторизации. - Сделаем первоначальную вёрстку. Добавим по центру экрана,
SingleChildScrollViewв котором будет форма содержащая иконку-логотип, текстовое поле для логина, текстовое поле для пароля, кнопка войти и кнопка зарегистрироваться.
Файл screens/login_screen.dart
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Здесь мы будем описывать, как выглядит наш экран
// Пока что вернем простой контейнер
return Container();
}
}
Создание базовой структуры экрана
Теперь давайте набросаем основные элементы на нашем экране. Мы хотим видеть:
- Заголовок "Суши Shop" сверху.
- По центру: иконку-логотип (пока поставим стандартную), поля для логина и пароля, и две кнопки.
Файл screens/login_screen.dart
// ----- ШАГ 0 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
),
),
);
}
}
// ----- ШАГ 1 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
),
),
),
);
}
}
// ----- ШАГ 2 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
// Добавляем Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
],
),
),
),
),
);
}
}
// ----- ШАГ 3 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
// Добавляем Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Добавляем логотип
Image.asset('assets/images/logo.png', width: 200, height: 200),
const SizedBox(height: 30), // Добавляем отступ
],
),
),
),
),
);
}
}
// ----- ШАГ 4 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
// Добавляем Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Добавляем логотип
Image.asset('assets/images/logo.png', width: 200, height: 200),
const SizedBox(height: 30), // Добавляем отступ
// Добавляем TextFormField для логина
TextFormField(
decoration: const InputDecoration(
labelText: 'Логин',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 20),
],
),
),
),
),
);
}
}
// ----- ШАГ 5 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
// Добавляем Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Добавляем логотип
Image.asset('assets/images/logo.png', width: 200, height: 200),
const SizedBox(height: 30), // Добавляем отступ
// Добавляем TextFormField для логина
TextFormField(
decoration: const InputDecoration(
labelText: 'Логин',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 20),
// Добавляем TextFormField для пароля
TextFormField(
decoration: const InputDecoration(
labelText: 'Пароль',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
),
const SizedBox(height: 20),
],
),
),
),
),
);
}
}
// ----- ШАГ 6 -----
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@override
Widget build(BuildContext context) {
// Добавляем Scaffold и AppBar
return Scaffold(
appBar: AppBar(
centerTitle: true, // Заголовок по центру
title: const Text('Суши Shop'),
// Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
// По центру будет прокручиваемый список
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
// Добавляем форму
child: Form(
// Добавляем Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Добавляем логотип
Image.asset('assets/images/logo.png', width: 200, height: 200),
const SizedBox(height: 30), // Добавляем отступ
// Добавляем TextFormField для логина
TextFormField(
decoration: const InputDecoration(
labelText: 'Логин',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 20),
// Добавляем TextFormField для пароля
TextFormField(
decoration: const InputDecoration(
labelText: 'Пароль',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
),
const SizedBox(height: 20),
// Добавляем кнопки "Войти" и "Зарегистрироваться"
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
textStyle: const TextStyle(fontSize: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
)
),
onPressed: () {},
child: const Text('Войти'),
),
const SizedBox(height: 10),
TextButton(
onPressed: () {},
child: const Text('Зарегистрироваться'),
),
],
),
),
),
),
);
}
}
Обновление маршрутизации
Обновление маршрутов
В файле main.dart исправим маршруты в таблице маршрутизации
Файл main.dart
void main() => runApp(const RollApp());
class RollApp extends StatelessWidget {
const RollApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Суши Shop", // Название приложения
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
initialRoute: '/login', // ⭠ Начинаем с экрана входа
routes: {
'/login': (context) => const LoginScreen(), // ⭠ Вот наш экран входа
'/main': (context) => MainScreen(),
'/detail': (context) => DetailRollScreen(),
'/cart': (context) => CartScreen(),
},
);
}
}
Добавление логики авторизации
Оживляем форму входа:
- Добавим контроллеры для управления текстовыми полями
- Добавим глобальный ключ для всей формы (нужно для валидации)
- Добавим тестовые временные логин и пароль
- Сделаем методы валидаторы
Для проверки мы временно создадим "правильные" логин и пароль прямо в коде.
ВАЖНО
Хранить логины и пароли вот так, прямо в коде (_correctLogin = "123";), в настоящих, серьезных приложениях – это ОГРОМНАЯ ДЫРА В БЕЗОПАСНОСТИ!
😱 Так делать категорически нельзя для приложений, которые будут использовать реальные люди.
Мы делаем это сейчас ТОЛЬКО для учебных целей, чтобы упростить пример. В реальном мире для аутентификации используются серверы и специальные безопасные протоколы.
Файл screens/login_screen.dart - Добавление контроллеров и логики
import 'package:flutter/material.dart';
import 'package:sushi_shop_app/screens/main_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
@c_start_1
// ⟶ Создаем контроллеры для текстовых полей
// Каждый контроллер "привязывается" к своему TextFormField
final TextEditingController _loginController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@c_end_1
@c_start_2
// ⟶ Создаем ключ для формы. Он позволит нам обращаться к состоянию Form.
final _formKey = GlobalKey();
@c_end_2
@c_start_3
// ⟶ Переменная для хранения сообщения об ошибке.
// Знак '?' означает, что _errorMessage может быть null
String? _errorMessage;
@c_end_3
@c_start_4
// ⟶ Для простоты, "правильные" логин и пароль
final String _correctLogin = "123";
final String _correctPassword = "123";
@c_end_4
@c_start_5
// ⟶ Функция для проверки введенных данных
void _login() {
setState(() {
_errorMessage = null; // Сброс сообщения об ошибке
});
if (_formKey.currentState!.validate()) {
if (_loginController.text.trim() == _correctLogin &&
_passwordController.text.trim() == _correctPassword) {
// Успешный вход
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainScreen()),
);
} else {
// Неверный логин или пароль
setState(() {
_errorMessage = "Неверный логин или пароль";
});
}
}
}
@c_end_5
@c_start_6
// ⟶ Функция для регистрации
void _register() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Регистрация ещё не реализована')),
);
}
@c_end_6
@c_start_7
// ⟶ Не забываем очистить контроллеры при закрытии экрана
@override
void dispose() {
_loginController.dispose();
_passwordController.dispose();
super.dispose();
}
@c_end_7
@override
Widget build(BuildContext context) {
// ... весь остальной код ...
}
}
Функция _login() - объяснение
Что делает функция _login()
Функция, которая будет срабатывать при нажатии кнопки "Войти". Эта функция:
- Сбросит предыдущее сообщение об ошибке.
- Запустит валидацию всех полей формы.
- Если валидация прошла успешно:
- Сравнит введенные данные с нашими "секретными"
_correctLoginи_correctPassword. - Если все совпало – ура! Переходим на главный экран. Для этого используем
Navigator.pushReplacement. ПочемуpushReplacement? Он заменяет текущий экран (логина) новым (главным). Это значит, что пользователь не сможет нажать кнопку "назад" на телефоне и вернуться на экран логина после успешного входа. - Если не совпало – покажем сообщение об ошибке.
- Сравнит введенные данные с нашими "секретными"
- Чтобы пользователь увидел изменения (например, новое сообщение об ошибке), нам нужно вызывать
setState(() { ... }). Эта команда говорит Flutter: "Эй, тут кое-что изменилось, перерисуй-ка экран!"
Связываем все вместе в UI
Мы подготовили всю логику, теперь нужно "привязать" ее к виджетам в методе build():
- Присвоить
_formKeyнашему виджетуForm. - Присвоить
_loginControllerи_passwordControllerсоответствующимTextFormField. - Добавить функции-валидаторы (
validator) для каждогоTextFormField.- Как работает
validator? Это функция, которая принимает текущее значение поля (value). Если значение корректно, функция должна вернутьnull. Если некорректно – она должна вернуть строку с текстом ошибки, которая отобразится под полем.
- Как работает
- Отобразить наше сообщение
_errorMessage(если оно есть). - Привязать методы
_loginи_registerк событиямonPressedнаших кнопок.
Файл screens/login_screen.dart - Полная реализация UI
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Sushi Shop'),
automaticallyImplyLeading: false,
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Form(
@c_start_1
key: _formKey, // -> Добавляем ключ для формы
@c_end_1
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.food_bank_outlined,
size: 100,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 30),
TextFormField(
@c_start_2
//-> Добавляем контроллер для текстового поля
controller: _loginController,
@c_end_2
decoration: const InputDecoration(
labelText: 'Логин',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.person),
),
@c_start_3
// -> Добавляем валидатор для текстового поля
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Пожалуйста, введите логин';
}
return null;
},
@c_end_3
),
const SizedBox(height: 20),
TextFormField(
@c_start_4
// -> Добавляем контроллер для текстового поля
controller: _passwordController,
@c_end_4
decoration: const InputDecoration(
labelText: 'Пароль',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
@c_start_5
// -> Добавляем валидатор для текстового поля
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Пожалуйста, введите пароль';
}
return null;
},
@c_end_5
),
const SizedBox(height: 10),
@c_start_6
// -> Выводим сообщение об ошибке, если оно есть
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 14),
textAlign: TextAlign.center,
),
),
@c_end_6
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
textStyle: const TextStyle(fontSize: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
@c_start_7
onPressed: _login, // -> Проверка авторизации
@c_end_7
child: const Text('Войти'),
),
const SizedBox(height: 10),
TextButton(
@c_start_8
onPressed: _register, // -> Регистрация
@c_end_8
child: const Text('Зарегистрироваться'),
),
],
),
),
),
),
);
}
Демонстрация работы приложения
Теперь можно протестировать наше приложение:
- Попробуй нажать "Войти", не вводя ничего. Должны появиться сообщения под полями.
- Введи только логин, но не пароль.
- Введи неверный логин/пароль (например, "test" / "test"). Должно появиться красное сообщение "Неверный логин или пароль".
- Введи правильные данные ("123" / "123"). Ты должен перейти на другой экран (если
MainScreenнастроен и существует, иначе приложение может выдать ошибку или ничего не сделать – это нормально на данном этапе, еслиMainScreenеще не готов). - Нажми "Зарегистрироваться". Должно появиться сообщение внизу экрана.
Полный код экрана авторизации
Файл screens/login_screen.dart - Финальная версия
import 'package:flutter/material.dart';
import 'package:sushi_shop_app/screens/main_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
// ⟶ Создаем контроллеры для текстовых полей
final TextEditingController _loginController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// ⟶ Создаем ключ для формы и переменную для сообщения об ошибке
final _formKey = GlobalKey();
String? _errorMessage;
// ⟶ Для простоты, "правильные" логин и пароль
final String _correctLogin = "123";
final String _correctPassword = "123";
// ⟶ Функция для проверки введенных данных
void _login() {
setState(() {
_errorMessage = null; // Сброс сообщения об ошибке
});
if (_formKey.currentState!.validate()) {
if (_loginController.text.trim() == _correctLogin &&
_passwordController.text.trim() == _correctPassword) {
// Успешный вход
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainScreen()),
);
} else {
// Неверный логин или пароль
setState(() {
_errorMessage = "Неверный логин или пароль";
});
}
}
}
// ⟶ Функция для регистрации
void _register() {
// TODO: Реализовать логику регистрации
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Функция регистрации пока не реализована')),
);
}
// ⟶ Не забываем очистить контроллеры при закрытии экрана
@override
void dispose() {
_loginController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Sushi Shop'),
// ⟶ Убираем кнопку назад, если это начальный экран
automaticallyImplyLeading: false,
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Form(
key: _formKey, // ⟶ Добавляем ключ для формы
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset('assets/images/logo.png', width: 200, height: 200),
const SizedBox(height: 30),
TextFormField(
// ⟶ Добавляем контроллер для текстового поля
controller: _loginController,
decoration: const InputDecoration(
labelText: 'Логин',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.person),
),
// ⟶ Добавляем валидатор для текстового поля
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Пожалуйста, введите логин';
}
return null;
},
),
const SizedBox(height: 20),
TextFormField(
// ⟶ Добавляем контроллер для текстового поля
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Пароль',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
// ⟶ Добавляем валидатор для текстового поля
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Пожалуйста, введите пароль';
}
return null;
},
),
const SizedBox(height: 10),
// ⟶ Выводим сообщение об ошибке, если оно есть
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 14),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 15),
textStyle: const TextStyle(fontSize: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
onPressed: _login, // ⟶ Добавляем функцию провеки авторизации
child: const Text('Войти'),
),
const SizedBox(height: 10),
TextButton(
onPressed: _register, // ⟶ Добавляем функцию регистрации
child: const Text('Зарегистрироваться'),
),
],
),
),
),
),
);
}
}