Разработка приложения Sushi App. Часть 2

Форма авторизации

Продолжаем дорабатывать приложение по заказу роллов и суши. Сейчас мы создадим один из самых важных экранов любого приложения – экран входа (или авторизации)

Что сделаем и чему научимся:

  1. Создадим новый экран login_screen.dart
  2. Разместим на нем поля для ввода логина и пароля, а также кнопки "Войти" и "Зарегистрироваться".
  3. Научимся "читать" данные из полей ввода.
  4. Добавим проверку (валидацию): если поля пустые или данные неверны, пользователь не сможет войти.
  5. При успешном входе – перенаправим пользователя на главный экран приложения.
1
Файл 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()

Функция, которая будет срабатывать при нажатии кнопки "Войти". Эта функция:

  1. Сбросит предыдущее сообщение об ошибке.
  2. Запустит валидацию всех полей формы.
  3. Если валидация прошла успешно:
    • Сравнит введенные данные с нашими "секретными" _correctLogin и _correctPassword.
    • Если все совпало – ура! Переходим на главный экран. Для этого используем Navigator.pushReplacement. Почему pushReplacement? Он заменяет текущий экран (логина) новым (главным). Это значит, что пользователь не сможет нажать кнопку "назад" на телефоне и вернуться на экран логина после успешного входа.
    • Если не совпало – покажем сообщение об ошибке.
  4. Чтобы пользователь увидел изменения (например, новое сообщение об ошибке), нам нужно вызывать setState(() { ... }). Эта команда говорит Flutter: "Эй, тут кое-что изменилось, перерисуй-ка экран!"

Связываем все вместе в UI

Мы подготовили всю логику, теперь нужно "привязать" ее к виджетам в методе build():

  1. Присвоить _formKey нашему виджету Form.
  2. Присвоить _loginController и _passwordController соответствующим TextFormField.
  3. Добавить функции-валидаторы (validator) для каждого TextFormField.
    • Как работает validator? Это функция, которая принимает текущее значение поля (value). Если значение корректно, функция должна вернуть null. Если некорректно – она должна вернуть строку с текстом ошибки, которая отобразится под полем.
  4. Отобразить наше сообщение _errorMessage (если оно есть).
  5. Привязать методы _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('Зарегистрироваться'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

Демонстрация работы приложения

Теперь можно протестировать наше приложение:

Демонстрация формы авторизации

Полный код экрана авторизации

Файл 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('Зарегистрироваться'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}