Профессиональная кроссплатформенная разработка на Python с использованием Flet

Курс ориентирован на опытных Python-разработчиков, желающих освоить создание адаптивных графических интерфейсов для Desktop, Mobile и Web. Программа охватывает путь от архитектуры страницы до построения сложных асинхронных систем с продвинутой анимацией и деплоем.

1. Основы Flet и архитектура Page: жизненный цикл и структура приложения

Основы Flet и архитектура Page: жизненный цикл и структура приложения

Представьте, что вы создаете интерфейс, который должен одинаково безупречно работать как в браузере Safari на iPhone, так и в нативном окне Windows 11, не требуя при этом переписывания ни единой строчки кода, отвечающего за логику. В мире Python-разработки долгое время существовал разрыв между мощными бэкенд-инструментами и фронтенд-фреймворками. Flet устраняет этот барьер, предлагая архитектуру, в которой Python управляет Flutter-движком через протокол обмена данными, превращая разработку UI в манипуляцию иерархическим деревом объектов.

Философия Flet: почему это не просто обертка над Flutter

Большинство кроссплатформенных фреймворков заставляют разработчика выбирать между нативностью и скоростью разработки. Flet занимает уникальную нишу. Технически он представляет собой сервер (написанный на Go), который управляет клиентом (написанным на Dart/Flutter). Когда вы запускаете приложение Flet, вы запускаете Python-скрипт, который общается с отрисовщиком через TCP-сокеты или веб-сокеты.

Это фундаментальное отличие от подходов вроде Kivy или PySide. В PySide вы работаете с биндингами к C++ библиотекам, что требует компиляции под каждую платформу. В Flet ваш Python-код остается неизменным, а «тяжелую» работу по рендерингу берет на себя Flutter-клиент.

Главная ментальная модель, которую нужно принять: Page (страница) — это не просто окно, это сессия. Для десктопного приложения это одно окно, но для веб-приложения — это конкретное подключение пользователя. Если десять человек откроют ваш сайт на Flet одновременно, сервер создаст десять независимых объектов Page, каждый со своим состоянием и жизненным циклом.

Анатомия приложения и точка входа

Любое профессиональное приложение на Flet начинается с функции main. Это не просто соглашение, это обязательная сигнатура для инициализации событийного цикла.

Функция ft.app() — это диспетчер. Она принимает аргумент target, который указывает на вашу основную логику. Важно понимать разницу в способах запуска:

  • Desktop-режим: По умолчанию ft.app(target=main) открывает нативное окно.
  • Web-режим: ft.app(target=main, view=ft.AppView.WEB_BROWSER) запускает локальный веб-сервер и открывает вкладку в браузере.
  • Объект page является корневым контейнером. Все, что вы видите на экране, находится внутри page.controls. Это список, и манипуляция этим списком — основной способ изменения интерфейса.

    Жизненный цикл страницы и событийная модель

    Понимание жизненного цикла Page критично для управления ресурсами, например, для закрытия соединений с базой данных или остановки фоновых задач при закрытии окна.

    Когда пользователь запускает приложение, происходит следующая последовательность:

  • Инициализация сессии: Flet создает экземпляр Page.
  • Вызов target-функции: Выполняется ваш код внутри main.
  • Построение дерева контролов: Вы добавляете элементы через page.add() или page.controls.append().
  • Отрисовка (Update): Изменения отправляются клиенту только после вызова page.update().
  • Важнейшим аспектом является обработка закрытия страницы. В Flet есть событие on_close, но оно специфично для веб-сессий. Для десктопа чаще используется on_window_event, который позволяет перехватить попытку закрытия окна:

    Событийная модель Flet построена на колбэках. Каждое событие (клик, ввод текста, изменение размера окна) порождает объект ControlEvent, содержащий:

  • target: Имя (ID) контрола, вызвавшего событие.
  • name: Тип события (например, "click").
  • data: Дополнительные данные (например, индекс выбранной вкладки).
  • Иерархия контролов: Дерево против Списка

    Во Flet интерфейс строится как дерево. Page — это корень. Внутри него могут быть View (для навигации), внутри ViewAppBar, FloatingActionButton и основной контент в controls.

    Рассмотрим структуру сложного интерфейса:

  • Page
  • - AppBar (Заголовок) - NavigationRail (Боковое меню) - Container (Основная область) - Column (Вертикальный стек) - Row (Горизонтальный ряд) - Text - Icon - ListView (Прокручиваемый список)

    Ключевой нюанс: Flet минимизирует трафик между Python и отрисовщиком. Когда вы меняете свойство контрола, например, text_field.value = "New Value", визуально ничего не меняется до вызова page.update(). Этот метод собирает все изменения («диффы») и отправляет их одним пакетом. В высоконагруженных интерфейсах вызов update() после каждого изменения — это антипаттерн. Правильнее изменить 10 свойств и вызвать update() один раз.

    Глубокое погружение в объект Page

    Объект ft.Page обладает огромным набором свойств, определяющих поведение всего приложения.

    Геометрия и параметры окна

    Для десктопных приложений управление окном — первоочередная задача.
  • page.window_width и page.window_height: Установка размеров.
  • page.window_resizable: Запрет или разрешение изменения размера.
  • page.window_always_on_top: Поверх всех окон.
  • page.window_top, page.window_left: Позиционирование на экране.
  • Интересной особенностью является работа с адаптивностью на уровне страницы. Свойство page.platform позволяет узнать, где запущено приложение (ios, android, macos, windows, linux, web). Это позволяет динамически менять интерфейс:

    Выравнивание и отступы

    Многие начинающие разработчики пытаются центрировать элементы, оборачивая их в бесконечные контейнеры. Однако Page сам по себе является мощным контейнером:
  • page.vertical_alignment: Выравнивание по вертикали (MainAxisAlignment).
  • page.horizontal_alignment: Выравнивание по горизонтали (CrossAxisAlignment).
  • page.padding: Внутренние отступы всей страницы.
  • Пример идеально центрированного контента:

    Управление состоянием на уровне страницы

    Flet предоставляет встроенные механизмы для хранения данных сессии. Это избавляет от необходимости использовать глобальные переменные, которые могут «протекать» между разными пользователями в веб-версии.

    page.session

    Это словарь, который живет ровно столько, сколько открыта страница.

    page.client_storage

    Если session очищается после обновления страницы в браузере, то client_storage использует локальное хранилище браузера (LocalStorage) или файл на диске для десктопа. Это идеальное место для хранения токенов авторизации или настроек темы.

    Важно помнить: данные в client_storage сериализуются в JSON, поэтому туда нельзя сохранять сложные Python-объекты (например, экземпляры классов с методами).

    Асинхронность в архитектуре Page

    Поскольку Flet часто используется для приложений, взаимодействующих с API или базами данных, блокировка основного потока недопустима. Если вы выполните time.sleep(10) внутри main, интерфейс «зависнет» для пользователя.

    Flet поддерживает asyncio из коробки. Для этого достаточно объявить main как async:

    При использовании асинхронного main, Flet автоматически запускает событийный цикл. Это позволяет легко интегрировать библиотеки вроде httpx или motor (MongoDB) для создания реактивных интерфейсов.

    Паттерны структурирования кода

    Для небольших скриптов достаточно одной функции main. Но профессиональная разработка требует разделения ответственности. Существует два основных подхода к организации кода во Flet:

    Функциональный подход (Composition)

    Вы разбиваете интерфейс на функции, возвращающие контролы.

    Классовый подход (Encapsulation)

    Создание кастомных контролов путем наследования от ft.UserControl (в новых версиях рекомендуется наследоваться от конкретных контейнеров, например, ft.Container или ft.Column). Это позволяет инкапсулировать логику внутри компонента.

    Использование классов делает код тестируемым и повторно используемым. Вы можете создать библиотеку собственных компонентов и использовать их в разных проектах.

    Оптимизация обновлений: метод update() и его нюансы

    Одной из самых частых ошибок является избыточный вызов page.update(). Когда ваше приложение растет, дерево контролов становится тяжелым. Вызов page.update() заставляет Flet проверять всё дерево на наличие изменений.

    Чтобы оптимизировать работу:

  • Локальный update: Если вы изменили свойство внутри конкретного Container, вызывайте container.update(), а не page.update(). Это обновит только эту ветку дерева.
  • Пакетное обновление: Группируйте изменения.
  • Использование page.controls.clear(): При полной смене экрана (например, переход из логина в личный кабинет) очищайте список контролов перед добавлением новых, чтобы старые объекты удалялись из памяти.
  • Особенности работы с Overlay

    Иногда элементы должны отображаться «над» основным контентом, но не быть частью стандартного потока верстки (например, диалоговые окна, снекбары или контекстные меню). Для этого у Page есть свойство overlay.

    Элементы в overlay не занимают места в макете, что критично для создания современных UI-эффектов.

    Граничные случаи: когда Flet ведет себя иначе

    Необходимо учитывать различия между Web и Desktop версиями в контексте Page:

  • Шрифты: В десктоп-версии вы можете использовать системные шрифты. В веб-версии их нужно предварительно загружать через page.fonts, иначе браузер заменит их на стандартные.
  • Пути к файлам: В десктопе C:\Users\... работает, в вебе — нет. Всегда используйте относительные пути и папку assets.
  • Горячие клавиши: События клавиатуры (page.on_keyboard_event) работают везде, но системные сочетания (типа Alt+F4) перехватываются операционной системой до того, как попадут во Flet.
  • Завершая разбор основ, важно подчеркнуть: Page — это ваш холст и одновременно дирижер. Понимание того, как данные текут от Python к Flutter-клиенту через дерево контролов и как update() синхронизирует эти состояния, является фундаментом для освоения более сложных тем — адаптивной верстки и навигации.

    2. Управление макетом и адаптивная верстка: работа с контейнерами и гибкими сетками

    Управление макетом и адаптивная верстка: работа с контейнерами и гибкими сетками

    Знаете ли вы, что разница между «наколеночным» скриптом и профессиональным приложением часто заключается не в сложности бизнес-логики, а в поведении интерфейса при изменении размера окна? В мире кроссплатформенной разработки, где одно и то же приложение должно одинаково успешно работать на 32-дюймовом 4K-мониторе и на компактном экране iPhone SE, жесткая привязка координат — это путь к катастрофе. Flet, построенный на движке Flutter, предлагает мощную систему декларативного лейаута, которая позволяет описывать не «где» находится элемент, а «как» он должен себя вести относительно соседей и родителя.

    Философия контейнеров и «коробочная» модель

    Во Flet практически всё, что вы видите на экране, подчиняется правилам вложенности. Однако базовый объект Page не является бездонным мешком. Чтобы управлять расположением элементов, нам нужны специализированные контролы-контейнеры. Самый фундаментальный из них — ft.Container.

    Если ft.Column или ft.Row отвечают за распределение детей в пространстве, то ft.Container — это швейцарский нож для оформления единичного блока. Его мощь раскрывается в пяти аспектах:

  • Геометрия и отступы: Различие между padding (внутренние отступы) и margin (внешние).
  • Декорирование: Скругление углов (border_radius), границы (border) и тени.
  • Трансформации: Поворот, масштабирование и смещение.
  • События: В отличие от многих других контролов, контейнер умеет слушать клики, наведение мыши и долгое нажатие.
  • Ограничения (Constraints): Возможность задать expand, width и height.
  • Рассмотрим критически важный нюанс: поведение expand. Когда вы устанавливаете expand=True (или целое число), контрол сообщает родителю (например, Row или Column), что он хочет занять всё доступное пространство. Если у двух соседних элементов expand=1 и expand=2, свободное пространство будет разделено в пропорции .

    > Во Flet свойство expand работает аналогично flex в CSS Flexbox или Expanded в Flutter. Это главный инструмент борьбы с «выпаданием» элементов за границы экрана.

    Линейные макеты: Row и Column

    Большинство интерфейсов строятся на комбинации строк и столбцов. На первый взгляд они кажутся простыми, но дьявол кроется в осях выравнивания.

    Каждый линейный макет имеет две оси:

  • Main Axis (Главная ось): Для Row это горизонталь, для Column — вертикаль.
  • Cross Axis (Поперечная ось): Перпендикулярна главной.
  • MainAxisAlignment (MAA)

    Этот параметр определяет, как распределяется лишнее пространство по главной оси.
  • START: Элементы прижаты к началу.
  • END: Элементы прижаты к концу.
  • CENTER: Группа элементов центрирована.
  • SPACE_BETWEEN: Первый и последний элементы прижаты к краям, остальные распределены равномерно.
  • SPACE_AROUND: Свободное место делится поровну, но у краев отступы в два раза меньше, чем между элементами.
  • SPACE_EVENLY: Все промежутки, включая края, строго равны.
  • CrossAxisAlignment (CAA)

    Определяет поведение элементов по поперечной оси.
  • START / END / CENTER.
  • STRETCH: Растягивает элементы на всю ширину (для Column) или высоту (для Row). Это критично для создания боковых панелей или кнопок на всю ширину экрана.
  • Пример сложного выравнивания:

    Адаптивность через ResponsiveRow

    Когда мы переходим от десктопа к мобильным устройствам, обычного Row становится недостаточно. Нам нужно, чтобы элементы «перепрыгивали» на новую строку или меняли свою ширину в зависимости от размера экрана. Для этого во Flet существует ResponsiveRow.

    ResponsiveRow использует 12-колоночную сетку, знакомую многим по Bootstrap. Каждый дочерний элемент может иметь атрибут col, который может быть:

  • Числом (например, col=6 — занимает половину ширины).
  • Словарём (например, col={"sm": 12, "md": 6, "lg": 4}).
  • Точки перелома (breakpoints) во Flet стандартны:

  • xs: 0 (мобильные телефоны)
  • sm: 576px (крупные телефоны)
  • md: 768px (планшеты)
  • lg: 992px (ноутбуки)
  • xl: 1200px (десктопы)
  • xxl: 1400px (большие мониторы)
  • Рассмотрим механику: если вы укажете col={"sm": 12, "md": 6}, то на экране шириной 600px элемент займет всю ширину (12 колонок), а на экране 800px — ровно половину. Это позволяет создавать интерфейсы, которые автоматически превращаются из трехколоночного дашборда в вертикальный список.

    Нюансы ResponsiveRow

    В отличие от обычного Row, элементы в ResponsiveRow не требуют expand=True для заполнения ширины, так как их размер диктуется сеткой. Однако важно помнить про spacing и run_spacing.
  • spacing: Расстояние между колонками по горизонтали.
  • run_spacing: Расстояние между «строками» сетки по вертикали при переносе элементов.
  • Продвинутые контейнеры: Stack и ListView

    Иногда линейного расположения недостаточно. Нам нужно наложить один элемент на другой или эффективно отобразить список из 10 000 объектов.

    Stack (Стопка)

    Stack позволяет размещать контролы друг над другом по оси Z. Это незаменимо для:
  • Наложения текста на изображение.
  • Создания кастомных индикаторов уведомлений (красная точка поверх иконки).
  • Сложных фоновых эффектов.
  • Внутри Stack мы используем позиционирование через свойства контрола top, bottom, left, right. Если эти свойства не заданы, элемент помещается в верхний левый угол.

    ListView и GridView

    Если вы добавите 1000 контейнеров в обычный Column, приложение начнет «тормозить», так как Flet попытается отрисовать их все сразу. ListView и GridView решают эту проблему через ленивую загрузку (виртуализацию).
  • ListView: Оптимизирован для длинных списков. Параметр spacing задает отступы, а auto_scroll позволяет реализовать поведение чата.
  • GridView: Идеален для галерей. Параметры max_extent или runs_count определяют, сколько элементов будет в ряду.
  • > Важный совет по производительности: Всегда используйте ListView вместо Column с scroll=True, если количество элементов заранее неизвестно или превышает 50-100 штук.

    Динамическая адаптация: Page.on_resize

    Иногда возможностей ResponsiveRow не хватает. Например, вы хотите полностью скрыть боковую панель и заменить её на FloatingActionButton только на мобильных устройствах. Для этого используется событийная модель.

    Объект page имеет событие on_resize. Внутри обработчика мы можем проверять page.width и программно изменять дерево контролов.

    Однако будьте осторожны: частые вызовы page.update() при каждом пикселе изменения размера могут вызвать мерцание. Рекомендуется использовать «пороги» (thresholds) для логики переключения.

    Работа с SafeArea и системными отступами

    При разработке под мобильные устройства (iOS/Android) возникает проблема «челок», вырезов под камеру и системных полосок навигации. Если вы просто разместите контейнер вверху страницы, часть контента может оказаться под часами или камерой.

    Во Flet управление этим происходит через page.padding или использование контрола ft.SafeArea. SafeArea — это контейнер, который автоматически добавляет необходимые отступы, чтобы контент не перекрывался системными элементами.

    Глубокая настройка геометрии: Constraints

    Каждый контрол во Flet имеет скрытый механизм «ограничений» (Constraints). Когда родитель (например, окно браузера) говорит контейнеру: «У тебя есть 500px ширины», контейнер передает это ограничение своим детям.

    Иногда нам нужно жестко ограничить размер, независимо от родителя. Для этого у Container есть свойства:

  • min_width / max_width
  • min_height / max_height
  • Представьте, что вы верстаете карточку товара. Вы хотите, чтобы она растягивалась, но не становилась шире 400px на огромных мониторах, иначе она будет выглядеть нелепо. Установка max_width=400 в сочетании с центрированием родительского контейнера — стандартное решение этой задачи.

    Практический пример: Адаптивный Dashboard

    Давайте соберем знания воедино и спроектируем структуру типичного рабочего стола.

  • Верхний уровень: ft.Row с expand=True.
  • Левая часть: Навигационное меню (ft.NavigationRail). Мы установим его visible=True только если page.width > 700.
  • Правая часть: Основной контент в ft.Column, обернутый в ft.Container с expand=True.
  • Сетка контента: Внутри основного столбца используем ft.ResponsiveRow для карточек с данными.
  • В этом примере мы использовали комбинацию Row для глобального разделения, NavigationRail для навигации, ResponsiveRow для сетки карточек и событие on_resize для управления видимостью элементов.

    Математика гибкости: Расчет пропорций

    Когда мы используем expand, полезно понимать, как именно распределяются пиксели. Допустим, у нас есть Row шириной пикселей. В нем три элемента:

  • Контейнер с фиксированной шириной .
  • Контейнер с expand=1.
  • Контейнер с expand=3.
  • Сначала Flet вычитает фиксированные размеры: . Затем рассчитывается общая сумма коэффициентов расширения: . Цена одной единицы expand составит пикселей. Итого: второй элемент получит пикселей, а третий — пикселей.

    Этот механизм позволяет создавать интерфейсы, где важный контент занимает львиную долю экрана, а вспомогательные элементы сохраняют читаемость.

    Тонкости отступов: Margin vs Padding

    Частая ошибка начинающих — путаница между margin и padding.

  • Padding: Это пространство внутри границ контейнера. Если у контейнера есть фоновый цвет, паддинг будет закрашен этим цветом. Он отодвигает content от краев контейнера.
  • Margin: Это пространство снаружи контейнера. Оно отодвигает сам контейнер от соседей. Фоновый цвет контейнера на марджин не распространяется.
  • Во Flet эти свойства принимают объект ft.padding.Padding или ft.margin.Margin. Вы можете задавать их разными способами:

  • ft.padding.all(10): со всех сторон.
  • ft.padding.only(left=10, top=20): выборочно.
  • ft.padding.symmetric(vertical=10, horizontal=5): парами.
  • Правильное использование этих свойств избавляет от необходимости вставлять пустые ft.Container или ft.Divider только ради создания визуальных пауз.

    Скроллинг и переполнение

    Когда контент не помещается в отведенное пространство, возникает ошибка переполнения (в консоли Flutter это выглядит как желто-черная полоса, во Flet контент просто обрезается). Чтобы этого избежать, у Column и ListView есть свойство scroll.

    Режимы скроллинга:

  • None: Скроллинг отключен (по умолчанию).
  • AUTO: Появляется при необходимости.
  • ADAPTIVE: Меняет поведение в зависимости от платформы (плавный на мобильных, с полосой прокрутки на десктопе).
  • HIDDEN: Скроллинг работает, но полоса прокрутки скрыта.
  • ALWAYS: Полоса прокрутки видна всегда.
  • Для профессиональных приложений рекомендуется ADAPTIVE, так как он обеспечивает нативный пользовательский опыт (UX) на каждой операционной системе.

    Итоги проектирования макета

    Создание адаптивного интерфейса во Flet — это не поиск «магической кнопки», а последовательное применение иерархии контейнеров. Начинайте с крупных блоков (Row, Column), используйте expand для распределения весов, внедряйте ResponsiveRow для поддержки разных экранов и не забывайте про SafeArea на мобильных устройствах.

    Помните, что хороший макет — это тот, который «дышит». Не бойтесь свободного пространства и используйте MainAxisAlignment.SPACE_BETWEEN для создания визуального баланса. В следующей главе мы вдохнем жизнь в эти макеты, научив их реагировать на действия пользователя и внешние раздражители.

    3. Интерактивные элементы и обработка событий: создание отзывчивого пользовательского опыта

    Интерактивные элементы и обработка событий: создание отзывчивого пользовательского опыта

    Представьте, что вы нажимаете на физическую кнопку, но не чувствуете ни сопротивления пружины, ни щелчка, а свет в комнате загорается лишь спустя пять секунд. Это ощущение «мертвого» интерфейса — главная причина, по которой пользователи удаляют приложения. В цифровой среде отзывчивость определяется не только скоростью работы бэкенда, но и тем, как фронтенд реагирует на каждое микродвижение пользователя. Во Flet интерактивность строится на тонком балансе между декларативным описанием UI и императивной обработкой событий, где каждое действие — от клика до перемещения курсора — становится сигналом для изменения состояния.

    Анатомия событий во Flet: от сигнала до кадра

    Любое взаимодействие пользователя с интерфейсом порождает объект события ControlEvent. В отличие от многих GUI-фреймворков, где события могут «всплывать» (bubbling) по дереву элементов, во Flet реализована более строгая модель: событие жестко привязано к конкретному контролу, который его инициировал.

    Объект e (экземпляр ControlEvent), который передается в обработчик, содержит три критически важных поля:

  • target: Строковый ID контрола, вызвавшего событие.
  • name: Название события (например, click, change, submit).
  • data: Строковое значение, несущее полезную нагрузку (например, текст из поля ввода или координаты клика).
  • Важно понимать, что Flet — это клиент-серверная архитектура. Когда пользователь нажимает кнопку в браузере или мобильном приложении, событие сериализуется, отправляется по протоколу gRPC или WebSocket на ваш Python-сервер, обрабатывается там, и только после вызова page.update() изменения возвращаются клиенту. Эта задержка (latency) диктует правила разработки: обработчики должны быть максимально легкими.

    Текстовый ввод и динамическая валидация

    Контрол TextField — это не просто поле для текста, а сложный агрегатор состояний. Профессиональный подход к формам ввода требует работы сразу с несколькими типами событий: on_change, on_submit, on_blur и on_focus.

    Событие on_change срабатывает при каждом нажатии клавиши. Это идеальное место для «живой» валидации. Рассмотрим сценарий ввода номера телефона или суммы перевода. Если мы будем ждать нажатия кнопки «Отправить», пользователь может расстроиться, узнав об ошибке в первом символе спустя минуту заполнения формы.

    Использование error_text позволяет избежать создания лишних Text-контролов для вывода ошибок, так как TextField сам резервирует место под подсказку. Однако здесь кроется нюанс производительности: если ваша логика валидации включает запросы к БД или тяжелые вычисления, использование on_change приведет к лагам интерфейса из-за постоянной перерисовки при каждом символе. В таких случаях применяется паттерн «Debouncing» (задержка выполнения), который мы разберем в главе про асинхронность.

    Глубокая работа с кнопками и жестами

    Кнопки во Flet делятся на три основных типа: ElevatedButton, FilledButton и OutlinedButton. С точки зрения событий они идентичны, но их выбор влияет на визуальную иерархию. Однако стандартного события on_click часто недостаточно для создания по-настоящему тактильного интерфейса.

    Для сложных взаимодействий используется GestureDetector. Этот контрол не имеет собственного визуального представления, но он способен «оборачивать» любой другой элемент, наделяя его способностью распознавать:

  • on_tap_down / on_tap_up: начало и конец касания.
  • on_long_press: удержание (полезно для контекстных меню).
  • on_pan_update: перемещение (drag-and-drop или рисование).
  • on_hover: наведение курсора (только для десктопа и веба).
  • Реализация Drag-and-Drop механики

    Во Flet есть специализированные контролы Draggable и DragTarget, но понимание того, как реализовать это через GestureDetector, дает контроль над физикой процесса.

    Когда мы используем on_pan_update, объект события предоставляет delta_x и delta_y. Суммируя эти значения с текущими координатами контейнера внутри Stack, мы создаем плавное перемещение.

    Здесь и — это свойства left и top контрола в Stack. Важно помнить о границах: профессиональный интерфейс не должен позволять элементу «улететь» за пределы видимой области страницы.

    Выбор и переключение: Checkbox, Switch и Radio

    Работа с булевыми состояниями и списками выбора требует понимания того, как value контрола соотносится с его визуальным состоянием.

  • Checkbox и Switch: Оба используют тип bool. Разница лишь в семантике. Switch обычно инициирует немедленное действие (включение Wi-Fi), а Checkbox — выбор опции внутри формы.
  • RadioGroup: Это контейнерный подход. Событие on_change вешается на саму группу RadioGroup, а не на отдельные кнопки Radio. Это избавляет от необходимости проверять состояние каждой кнопки вручную.
  • Продвинутые события: Keyboard и Lifecycle

    Иногда взаимодействие происходит вне конкретного контрола. Flet позволяет подписываться на глобальные события страницы.

    Обработка горячих клавиш

    Событие page.on_keyboard_event — это мощный инструмент для десктопных приложений. Объект события KeyboardEvent содержит поля key, shift, ctrl, alt и meta. Это позволяет реализовывать привычные паттерны: Ctrl+S для сохранения, Esc для выхода из полноэкранного режима или Enter для отправки сообщения.

    Нюанс: если фокус находится в TextField, нажатие клавиш все равно пробрасывается в on_keyboard_event. Это может привести к двойному срабатыванию логики. Всегда проверяйте, не занят ли ввод в данный момент, если ваша горячая клавиша — это просто буква без модификаторов.

    События жизненного цикла и видимости

    Для мобильных устройств критически важно событие page.on_app_lifecycle_state_change. Оно сообщает, когда приложение ушло в фон (inactive, paused) или вернулось в активное состояние (resumed).

  • В состоянии paused стоит приостановить таймеры, остановить тяжелые анимации или закрыть WebSocket-соединения для экономии заряда батареи.
  • В состоянии resumed следует обновить данные, которые могли устареть, пока пользователь отсутствовал.
  • Индикация загрузки и обратная связь (Feedback)

    Интерактивность — это не только ввод, но и подтверждение того, что ввод принят. Если после нажатия кнопки «Оплатить» ничего не происходит в течение 200 мс, пользователь нажмет её еще пять раз.

    Стратегия обработки «тяжелых» событий:

  • Мгновенная реакция: В начале обработчика установите button.disabled = True и button.content = ft.ProgressRing(). Вызовите button.update().
  • Выполнение задачи: Запустите асинхронную функцию или расчет.
  • Завершение: Верните кнопку в исходное состояние, покажите SnackBar с результатом.
  • Работа с Hover и курсорами

    Для веб-версий и десктопа важно визуально выделять интерактивные зоны. Контролы во Flet поддерживают свойство mouse_cursor, которое может принимать значения ft.MouseCursor.POINTER, ft.MouseCursor.TEXT, ft.MouseCursor.WAIT и другие.

    Событие on_hover позволяет менять стиль элемента при наведении. Однако будьте осторожны: изменение размеров или отступов элемента при наведении может вызвать «эффект дрожания» (jitter), когда элемент увеличивается, курсор оказывается вне его границ, элемент уменьшается, курсор снова оказывается внутри, и цикл повторяется бесконечно. Для плавных изменений лучше использовать свойства opacity или color в сочетании с неявными анимациями (которые мы подробно разберем в будущих главах).

    События прокрутки и динамический контент

    Контролы со скроллом (ListView, Column с scroll=True) поддерживают событие on_scroll. Это открывает путь к реализации «бесконечных списков».

    Объект события прокрутки предоставляет:

  • pixels: текущая позиция в пикселях от начала.
  • min_scroll_extent / max_scroll_extent: границы прокрутки.
  • Алгоритм пагинации: если e.pixels > e.max_scroll_extent - 100, значит, пользователь почти доскроллил до конца, и пора подгружать следующую порцию данных. Это создает бесшовный опыт, где пользователь не сталкивается с жесткими границами страниц.

    Групповые операции и делегирование

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

    Хотя Flet не поддерживает классическое делегирование событий в стиле JS (event.target.closest), вы можете использовать замыкания или атрибут data контрола. Атрибут data — это универсальное хранилище любого Python-объекта (строки, словаря или даже экземпляра класса) прямо внутри контрола.

    Этот подход позволяет иметь одну функцию-обработчик для всего списка, что упрощает отладку и поддержку кода.

    Нюансы кроссплатформенности в обработке событий

    При проектировании интерактивности важно учитывать различия в устройствах ввода.

  • Правый клик: На десктопе это вызов контекстного меню. Во Flet это событие on_secondary_tap в GestureDetector. На мобильных устройствах оно часто эмулируется долгим нажатием, но полагаться на это на 100% нельзя.
  • Колесо мыши: Событие on_scroll_delta доступно в GestureDetector и критично для приложений с картами или графиками (зум). На тачскринах это заменяется жестом «щепок» (pinch), который требует более сложной обработки координат двух точек касания.
  • Фокус: На мобильных устройствах появление клавиатуры при фокусе на TextField изменяет размер доступной области page.window_height. Это событие стоит отслеживать, чтобы важные элементы управления не оказались перекрыты клавиатурой.
  • Проектирование состояний: от "Нажато" до "Выбрано"

    Интерактивность неразрывно связана с визуальным состоянием. Во Flet многие контролы имеют встроенную поддержку состояний. Например, ButtonStyle позволяет задавать разные цвета фона для состояний:

  • ft.ControlState.HOVERED
  • ft.ControlState.FOCUSED
  • ft.ControlState.PRESSED
  • ft.ControlState.DEFAULT
  • Использование ControlState — это наиболее производительный способ стилизации, так как эти изменения обрабатываются на стороне клиента (Flutter) без необходимости делать круг «клиент-сервер-клиент» через page.update(). Это обеспечивает мгновенный визуальный отклик, даже если интернет-соединение нестабильно.

    Финализация взаимодействия

    Создание отзывчивого интерфейса — это искусство предсказания ожиданий пользователя. Если элемент выглядит как кнопка, он должен нажиматься. Если он содержит текст, пользователь должен иметь возможность его выделить (свойство selectable=True). Если происходит длительный процесс, должна быть визуальная индикация.

    Помните, что каждое событие во Flet — это возможность подтвердить пользователю: «Я тебя слышу, я работаю». Правильное использование on_change для валидации, GestureDetector для сложных жестов и ControlState для мгновенного отклика превращает набор виджетов в профессиональный программный продукт.

    4. Стилизация и кастомные темы оформления: использование ThemeData, шрифтов и палитр

    Стилизация и кастомные темы оформления: использование ThemeData, шрифтов и палитр

    Цвет кнопки — это не просто эстетический выбор, это часть интерфейсного контракта с пользователем. В профессиональной разработке на Flet хаотичное назначение цветов (вроде bgcolor="blue") напрямую в коде контролов ведет к архитектурному тупику: проект становится невозможно поддерживать, а внедрение темной темы превращается в многочасовой рефакторинг. Системный подход к стилизации требует перехода от императивного задания свойств к декларативному описанию темы через объект ThemeData.

    Философия Material Design 3 в экосистеме Flet

    Flet базируется на Flutter, который, в свою очередь, является эталонной реализацией концепции Material Design 3 (M3). Понимание того, как устроена эта дизайн-система, критически важно для работы с темами. В M3 акцент смещен с фиксированных цветов на динамические цветовые схемы и роли.

    Когда вы задаете тему в page.theme, вы определяете не просто набор красок, а семантическую карту. Например, вместо «красного цвета» система оперирует понятием error. Если вы решите, что в вашем приложении ошибки должны подсвечиваться оранжевым, вам достаточно изменить одно значение в схеме, и все контролы (текстовые поля, иконки, снекбары), использующие роль error, обновятся автоматически.

    Объект ft.Theme содержит в себе три ключевых столпа стилизации:

  • ColorSheme: палитра из более чем 30 цветовых ролей.
  • TextTheme: типографическая сетка (заголовки, подзаголовки, основной текст).
  • VisualDensity: плотность интерфейса (насколько компактно расположены элементы).
  • Глобальная настройка ThemeData

    Настройка визуального облика начинается с атрибутов page.theme и page.dark_theme. Flet позволяет бесшовно переключаться между ними, меняя лишь свойство page.theme_mode.

    Использование color_scheme_seed — это самый быстрый способ получить гармоничный интерфейс. Алгоритм берет один базовый цвет и математически вычисляет все остальные оттенки (фон, поверхности, акценты), чтобы они соответствовали правилам контрастности.

    Тонкая настройка ColorScheme

    Если автоматической генерации недостаточно, мы можем определить ft.ColorScheme вручную. Это необходимо, когда брендбук компании диктует жесткие требования к конкретным оттенкам.

    | Роль в ColorScheme | Описание применения | | :--- | :--- | | primary | Главный акцентный цвет (основные кнопки, активные состояния). | | on_primary | Цвет контента (текста/иконок) поверх primary. | | surface | Цвет фонов карточек, диалогов и панелей. | | secondary | Менее заметные элементы (фильтры, чипы). | | outline | Цвет границ и разделителей. | | error | Индикация критических состояний. |

    Важно соблюдать правило парности: для каждого цвета фона есть соответствующий цвет "on" (на нем). Если вы установили primary=ft.colors.BLACK, то on_primary обязан быть светлым, иначе текст на кнопке станет нечитаемым.

    Работа с типографикой и кастомными шрифтами

    Шрифты — это голос вашего интерфейса. Flet поддерживает использование как стандартных системных шрифтов, так и загрузку кастомных файлов (TTF/OTF).

    Подключение внешних шрифтов

    Для использования своего шрифта его необходимо зарегистрировать в словаре page.fonts. Ключом будет имя, по которому вы будете обращаться к шрифту в коде, а значением — путь к файлу или URL.

    После регистрации шрифт можно применить глобально через ThemeData или локально в конкретном контроле ft.Text(font_family="BrandFont").

    Настройка TextTheme

    В Material Design 3 существует иерархия текстовых стилей. Вместо того чтобы каждый раз указывать size=24, weight="bold", профессиональнее настроить глобальные стили:

  • Display (Large, Medium, Small): для очень крупных заголовков на лендингах.
  • Headline: для заголовков разделов.
  • Title: для названий в Appbar или заголовков карточек.
  • Body: для основного контента.
  • Label: для подписей кнопок и мелких пояснений.
  • Пример переопределения базового стиля:

    Использование именованных стилей позволяет сохранять консистентность. Если завтра дизайнер решит увеличить межстрочный интервал во всем приложении, вы измените одну строку в text_theme, и изменения коснутся сотен экранов.

    Стилизация компонентов через ButtonStyle и Page-level Styles

    Иногда глобальной темы недостаточно, и нужно выделить группу элементов (например, все кнопки «Удалить» должны быть красными, независимо от основной темы). Для этого во Flet используются объекты стилей.

    Детальная настройка кнопок

    ft.ButtonStyle — это мощный инструмент, позволяющий управлять состоянием контрола. Мы можем менять свойства кнопки в зависимости от того, наведен ли на нее курсор, нажата ли она или заблокирована.

    Здесь используется словарь состояний. Это избавляет от необходимости писать обработчики on_hover вручную для простой смены цвета. Flet сам вычислит, какое состояние сейчас активно, и применит нужный стиль.

    Наследование стилей в контейнерах

    Контейнеры (ft.Container) — это скелет стилизации. Важно понимать разницу между bgcolor темы и bgcolor контейнера. Контейнер может принимать ft.LinearGradient или ft.SweepGradient, что недоступно для большинства базовых контролов.

    Адаптивная стилизация и динамические цвета

    Современные операционные системы (Android 12+, macOS, Windows 11) поддерживают концепцию системных акцентных цветов. Flet умеет считывать эти значения.

    Если вы установите page.theme = ft.Theme(color_scheme_seed=None), но при этом не зададите жестких цветов, Flet попытается адаптироваться под окружение. Однако для профессионального приложения лучше контролировать этот процесс.

    Динамическое переключение тем

    Реализация переключателя «День/Ночь» требует понимания того, как page.update() взаимодействует с объектами темы.

    При смене theme_mode Flet не просто перерисовывает экран, он выполняет интерполяцию цветов (если настроены анимации, которые мы разберем в будущих главах). Все контролы, которые используют стандартные цвета темы (например, color=ft.colors.ON_SURFACE), мгновенно изменят свой облик.

    Практическое применение: Создание дизайн-системы

    В крупных проектах рекомендуется выносить тему в отдельный файл theme.py. Это позволяет разделить логику интерфейса и визуальное описание.

    Рассмотрим структуру такого файла:

    Обратите внимание на scrollbar_theme. Стилизация скроллбаров — частая задача для десктопных приложений. Во Flet вы можете глобально настроить их вид, чтобы они не выбивались из общего дизайна.

    Использование прозрачности и наложений

    Работа с цветами во Flet поддерживает альфа-канал. Для этого используется метод ft.colors.with_opacity(opacity, color). Это критично для создания эффектов «стекла» (Glassmorphism) или затененных подложек.

    Например, создание полупрозрачного оверлея для модального окна:

    Параметр blur в сочетании с прозрачным bgcolor позволяет создавать современные, визуально глубокие интерфейсы, характерные для мобильных ОС последнего поколения.

    Нюансы кроссплатформенной стилизации

    При разработке под разные платформы (Web, Android, iOS, Windows) стоит учитывать, что рендеринг шрифтов и плотность пикселей могут отличаться.

  • Плотность (Visual Density): Для мобильных устройств лучше использовать VisualDensity.COMFORTABLE или STANDARD, чтобы области нажатия были достаточно большими. Для веб-админок с таблицами данных подойдет VisualDensity.COMPACT.
  • Системные шрифты: Если вы не используете кастомный шрифт, помните, что на iOS будет отображаться San Francisco, на Android — Roboto, а на Windows — Segoe UI. Если ваш дизайн «разваливается» при смене шрифта (например, текст не влезает в кнопку), обязательно задавайте фиксированный font_family.
  • Цвета в вебе: При запуске в браузере Flet использует цвета в формате HEX или стандартные константы Flutter. Если вы интегрируете приложение в существующий сайт, убедитесь, что page.bgcolor совпадает с фоном родительского iframe или контейнера.
  • Продвинутая работа с тенями и границами

    В Material Design 3 тени стали менее выраженными, уступив место цветовому тонированию поверхностей. Однако для создания иерархии (Elevation) они все еще необходимы.

    В объекте ThemeData можно настроить shadow_color, который будет использоваться всеми контролами по умолчанию. Но для индивидуальной настройки используется ft.BoxShadow.

    Параметр offset принимает объект ft.Offset(x, y), где и — смещение тени в логических пикселях. Положительное значение смещает тень вниз, создавая эффект того, что элемент «парит» над поверхностью.

    Замыкание концепции стилизации

    Стилизация во Flet — это не просто раскрашивание кнопок, а создание масштабируемой системы. Использование ThemeData позволяет отделить визуальные атрибуты от структуры приложения. Начиная проект, всегда определяйте color_scheme_seed и базовую text_theme. Это сэкономит десятки часов при расширении функционала и обеспечит профессиональный вид приложения на любой платформе, будь то экран смартфона или широкоформатный монитор.

    Локальные стили через ButtonStyle или свойства контейнеров должны использоваться как исключения для уникальных элементов, в то время как общая гармония должна диктоваться глобальным объектом темы. В следующей главе мы увидим, как эта система стилей интегрируется в механизм навигации и как сохранять визуальную целостность при переходах между разными экранами приложения.