Сегодня я покажу вам, что для создания полноценных кроссплатформенных приложений достаточно одного языка — Python. С помощью всего нескольких библиотек и фреймворков можно легко обойтись без JavaScript для веб-разработки, без Kotlin и Swift для мобильных приложений и даже без C++ для десктопных программ.
В этой статье мы разберем, как, используя Flet и FastAPI, можно создавать мощные и удобные решения для любой платформы. А в конце я объясню, почему пока лучше воздержаться от использования Flet.
Что такое Flet?
Flet — это современный фреймворк для разработки кроссплатформенных приложений на Python, вдохновленный мощью и гибкостью UI‑фреймворка Flutter от Google.
Flutter, впервые представленный в 2016 году, стремительно завоевывает популярность благодаря возможности создания приложений для всех основных платформ. Хотя многие ошибочно считают Flutter отдельным языком, он написан на Dart (тоже от компании Google) — языке, который компилируется как в машинный код для iOS и Android, так и в JavaScript для веб‑приложений, обеспечивая универсальность.
Flet, в свою очередь, переносит концепцию Flutter в Python, устраняя необходимость изучения Dart. Этот фреймворк позволяет создавать пользовательские интерфейсы на знакомом Python‑синтаксисе, что значительно ускоряет разработку и снижает порог вхождения для программистов, привыкших работать с Python.
В конце статьи я сделаю подробные выводы по Flet. Так что дочитайте до конца. Забегая вперёд: концепция обещает многое, но её практическая жизнеспособность пока вызывает сомнения.
Цель статьи
К концу этой статьи мы создадим два приложения: API с использованием FastAPI и фронтенд-часть с использованием Flet. После чего развернем проект в облаке Amvera, перетащив файлы проекта в интерфейсе, и активировав бесплатное доменное имя.
Наш API будет выполнять следующие функции:
Авторизация, аутентификация и регистрация пользователей.
Доступ к функционалу в зависимости от прав доступа.
Основная функциональная часть приложения (подробности далее).
Наше приложение на Flet будет включать:
Форму для авторизации и регистрации.
Основную функциональную часть (панель приложения).
Возможность сохранения токена авторизации во внутреннем хранилище Flet-приложения.
Проверку уровня доступа.
Функционал API
Помимо авторизации и аутентификации, API будет поддерживать отправку текстовых, фото и файловых сообщений в Telegram-бота. Для фото и файлов также добавим возможность отправки медиа с комментариями.
Таким образом, мы реализуем пользовательскую часть (Auth) и несколько специальных методов API для отправки сообщений в зависимости от прав доступа.
Для хранения данных будет использоваться база данных SQLite с асинхронным движком aiosqlite и ORM-фреймворком SQLAlchemy. Для управления миграциями — Alembic.
Универсальная заготовка для FastAPI-приложений
Чтобы ускорить написание кода, я поделюсь своей универсальной заготовкой для разработки FastAPI-приложений. Мы возьмем её за основу, добавлю краткие комментарии, и более детально разберем те части, которые предстоит написать.
В результате мы получим полноценный API, который можно интегрировать в любую систему, в том числе в наше Flet-приложение.
Для удобства я выполню деплой API на платформе Amvera Cloud, что займет буквально 5 минут. Amvera выбран благодаря простоте деплоя (можно развернуть за три команды в IDE или через загрузку файлов в интерфейсе) и бесплатному домену для каждого проекта.
Полученную ссылку можно будет использовать во Flet независимо от типа приложения. Ваш API будет одинаково хорошо работать в веб-версии, на десктопе или на мобильных устройствах.
Функционал Flet
Мы опишем всё в формате одностраничного приложения (Single Page Application). Это значит, что весь функционал будет перерисовываться в рамках одного экрана. Экран приложения будет содержать следующие компоненты:
Форму регистрации.
Форму авторизации.
Экран с уведомлением «У вас нет прав доступа к приложению».
Экран выбора действия: отправка фото, документа или сообщения.
Формы для отправки фото, текста и файлов.
В этой части я покажу, как отрисовывать компоненты, использовать токен, выполнять запросы к API и многое другое.
Трансформация кода в кроссплатформенные приложения
В завершение этого блока мы перейдем к самой важной теме — трансформации кода в полноценные веб-, десктопные и Android-приложения.
Мы рассмотрим два подхода: трансформацию «без заморочек» и «с заморочками».
Часть «Без заморочек»
Эта часть охватывает преобразование Flet-приложения в WebApp и Desktop. Мы не будем использовать дополнительное ПО, такое как Android Studio или Flutter. Для трансформации в WebApp достаточно средств самого Flet, а для преобразования в Desktop используем библиотеку Pyinstaller.
WebApp без деплоя неполноценный WebApp. Поэтому, мы не только запакуем все в сайт, но и выполним его деплой на том же Amvera.
Используя Pyinstaller нам достаточно будет ввести 1 команду и через пару минут получить полноценный exe файл. Справится каждый, а времени займет минимум.
Часть с заморочками
Эта часть для действительных фанатов и для тех кто идет до конца. В ее рамках я покажу вам как правильно паковать под Desktop и Android версию, но покажу не особо детально. По причине, что многие не дочитают, а те кто дочитает, возможно, не захочет заморачиваться всем этим.
В рамках этой части необходимо:
скачать Flutter
скачать Android Studio
скачать Visual Studio
скачать Visual Studio Code
скачать порядка 10–15GB пакетов Visual Studio
После необходимо все это добавить в переменные окружения вашей системы.
Затем выполнить команду «flutter doctor», которая скажет чего вам еще не хватает.
После, когда все уже будет установлено можно будет собирать специальными командами Flet. На этом этапе, лично меня, ожидало несколько неприятных сюрпризов из серии того что версия Python не подходит или что чего-то не хватает.
После завершения всех шагов можно будет собирать приложения, используя команды Flet.
Да, вы правильно поняли. Нам нужно установить софта на +-20GB, чтоб выполнить упаковку приложения Flat во что-то кроме WebApp версии, но об этом подробнее в блоке моих личных выводов по Flet и в моем мнении перспектив на его счет.
Пишем API
Теперь приступаем к написанию API.
В этом разделе я буду демонстрировать разработку API, используя собственную заготовку. Этот шаблон я активно поддерживаю и применяю в рабочих проектах.
По сути, данная заготовка — это готовый фреймворк для разработки масштабируемых веб-приложений на базе FastAPI с полной поддержкой аутентификации и авторизации. Проект построен на модульной архитектуре, включает гибкое логирование с использованием Loguru, асинхронное взаимодействие с базой данных через SQLAlchemy и удобную систему миграций, реализованную на Alembic, что значительно упрощает управление схемой базы данных.
Шаблон содержит универсальный класс для работы с базой данных, а также модули для кастомной авторизации, регистрации и аутентификации и многое другое. Он разработан для сокращения времени и усилий на подготовку инфраструктуры для новых проектов.
Если данный проект окажется полезным, поддержите его звездочкой на GitHub и напишите в комментариях, как он помог в вашей работе.
Ссылка на проект: FastAPI Template with Auth.
Я буду использовать этот шаблон для демонстрации, но, если вы уже опытны в FastAPI, можете использовать собственный проект. В таком случае можно сразу перейти к разделу, посвященному написанию кода на Flet.
Установка проекта
Для начала скопируем проект из репозитория на GitHub. Это позволит загрузить всю структуру и файлы проекта на ваш локальный компьютер, после чего можно будет запустить его и работать с ним.
Важно: на вашем компьютере должен быть установлен Git. Это необходимо для клонирования репозитория и управления версиями проекта. Если Git еще не установлен, вы можете загрузить и установить его, следуя инструкциям на официальном сайте Git. После установки вы сможете использовать команды Git из терминала.
Шаги для копирования проекта:
-
Клонирование репозитория:
Откройте терминал и выполните следующую команду, чтобы скопировать проект на свой компьютер:
git clone https://github.com/Yakvenalex/FastApiWithAuthSample
Эта команда создаст на вашем компьютере новую папку с названием проекта, в которой будет полная копия репозитория.
-
Переход в папку с проектом:
Перейдите в директорию проекта:
cd FastApiWithAuthSample
Теперь проект готов для установки зависимостей и дальнейших шагов по настройке.
Настройка связи с Telegram
В проекте будет использоваться связка с Telegram. Для удобства я разработал библиотеку, из которой нас будут интересовать некоторые методы. Чтобы эта библиотека установилась у вас, добавьте в файл requirements.txt
следующую строку:
easy_async_tg_notify==0.1
Далее, для установки всех зависимостей, выполните в терминале команду:
pip install -r requirements.txt
Коротко рассмотрим структуру проекта
В корне проекта находятся две папки:
data
app
Папка data
содержит данные, чувствительные к изменениям и сохранению. В базовой сборке — это файл базы данных SQLite.
Папка app
включает файлы структуры приложения. Часть из них мы сегодня изменим или добавим, а часть оставим без изменений.
Структура приложения (app)
Начнем с пакета dao
.
Данный пакет содержит три основных файла:
databa.py
: файл с базовыми настройками SQLAlchemy.session_maker.py
: универсальный файл для генерации сессий SQLAlchemy, как под FastAPI, так и под обычные приложения.base.py
: файл с универсальными методами для взаимодействия с базой данных (классBaseDao
).
В корне app
также есть папка migration
, которая связана с миграциями Alembic.
Подробно про SQLAlchemy и Alembic я писал в следующих статьях (добавьте ссылки):
Файл конфигурации (app/config.py)
Этот файл содержит переменные окружения из .env
и прочие зависимости, которые используются в проекте, с помощью библиотеки pydantic_settings.
Пример итоговой конфигурации:
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
DB_URL: str = f"sqlite+aiosqlite:///{BASE_DIR}/data/db.sqlite3"
BOT_TOKEN: str
CHAT_ID: int
SECRET_KEY: str
ALGORITHM: str
model_config = SettingsConfigDict(env_file=f"{BASE_DIR}/.env")
# Получаем параметры для загрузки переменных среды
settings = Settings()
database_url = settings.DB_URL
Обратите внимание на переменные BOT_TOKEN
, CHAT_ID
, SECRET_KEY
и ALGORITHM
, которые необходимо добавить в файл .env
(должен находиться в корне проекта, на один уровень выше папки app
).
Описание переменных .env
-
BOT_TOKEN — токен Telegram-бота, который можно получить через BotFather:
Откройте Telegram и найдите BotFather.
Начните чат с BotFather и отправьте команду
/newbot
.Следуйте инструкциям для создания бота.
Сохраните полученный токен.
CHAT_ID — ваш Telegram ID, на который API будет отправлять сообщения. Можно узнать, используя специального бота.
-
SECRET_KEY — секретный ключ для шифрования и расшифровки JWT-токенов. Он обеспечивает безопасность, так как подписывает токен. Пример ключа (рекомендуемая длина: 32 символа):
gV15m09aIzLKG4qpgVckdDddQbPQrtAO0nM-7Ywkjdas0XPydgKKjmckJAfgLkqJXEt
ALGORITHM — алгоритм подписи JWT-токена, в данном случае HS256.
Пример заполненного файла .env
:
SECRET_KEY=gV15m09aIzLKG4qpgVckdDddQbPQrtAO0nM-7Ywkjdas0XPydgKKjmckJAfgLkqJXEt
ALGORITHM=HS256
BOT_TOKEN=bot_token
CHAT_ID=5027041755
Модуль auth
В папке auth
находятся файлы, которые организуют логику аутентификации и авторизации в проекте.
auth.py — основной файл для настройки и управления аутентификацией и авторизацией, с методами
create_access_token
для генерации JWT-токена иauthenticate_user
для проверки входа пользователя.dao.py (Data Access Object) — файл с классами для взаимодействия с SQLAlchemy.
-
dependencies.py — файл для зависимостей, которые можно подключить к маршрутам. Новая строка, добавленная здесь:
from easy_async_tg_notify import Notifier async def get_notifier(): async with Notifier(settings.BOT_TOKEN) as notifier: yield notifier
models.py — описание моделей базы данных, связанных с аутентификацией, пользователями и ролями.
router.py — файл с маршрутами для авторизации и аутентификации.
schemas.py — описание схем данных (Pydantic-модели), используемых для валидации данных в API.
utils.py — вспомогательные функции для модуля аутентификации, включая хеширование и проверку паролей.
Примечание: добавлена зависимость для работы с Telegram API.
Запуск и тестирование
В терминале возвращаемся в корень проекта (на уровень выше папки app) и вводим:
uvicorn app.main:app --reload
Если все было заполнено корректно, то в терминале вы увидите следующее:
Для того чтоб убедиться, что все работает корректно можно перейти в документацию (http://127.0.0.1:8000/docs) и проверить методы на работоспособность.
Если вы копировали мой шаблон, то у вас уже должна быть база данных SQLite с пользователями с разными ролями. Можете работать с ними или добавить собственного пользователя.
Проверяем что все работает корректно и, если это так, можно переходить к написанию API методов конкретно под Flet.
Пишем API методы под Flet
В папке app
создайте новую папку api
и сразу добавьте в неё два пустых файла:
router.py: для описания кода наших API методов
schemas.py: для описания Pydantic-схем для API методов
Файл schemas.py:
from pydantic import BaseModel
class Message(BaseModel):
text: str
Хотя можно было бы оставить это в router.py
, вынос схемы в отдельный файл позволит удобнее организовать код для будущих, более сложных API.
Файл router.py
Рассмотрим его подробнее.
Импорты
import os # Для работы с операционной системой, например, для доступа к переменным окружения.
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
# APIRouter: для организации маршрутов API.
# UploadFile, File: для обработки загружаемых файлов.
# Depends: для инъекции зависимостей.
# HTTPException: для возврата HTTP-ошибок.
from easy_async_tg_notify import Notifier
# Notifier: для асинхронной отправки уведомлений в Telegram.
from app.api.schemas import Message
# Message: схема данных (Pydantic) для описания структуры сообщения.
from app.auth.dependencies import get_notifier, get_current_admin_user
# get_notifier: зависимость для уведомлений.
# get_current_admin_user: зависимость для проверки прав администратора.
from app.auth.models import User
# User: модель пользователя из модуля авторизации.
from app.config import settings
# settings: настройки приложения.
Создание роутера
router = APIRouter(prefix='/api', tags=['API'])
Описание эндпоинтов
Первый метод отправляет простое текстовое сообщение в Telegram-бот (себе).
@router.post('/send_text')
async def send_text(message: Message,
notifier: Notifier = Depends(get_notifier),
user_data: User = Depends(get_current_admin_user)):
try:
await notifier.send_text(message.text, settings.CHAT_ID)
return {"status": "success", "message": "Текстовое сообщение отправлено"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
-
Параметры:
message: Message
: тело запроса с текстом сообщения, проверяется с помощью схемыMessage
.notifier: Notifier = Depends(get_notifier)
: зависимость для объектаNotifier
, который отправляет сообщения в Telegram.user_data: User = Depends(get_current_admin_user)
: зависимость для проверки прав администратора (доступен только администратору).
Метод для отправки фото
@router.post('/send_photo')
async def send_photo(caption: str = None,
file: UploadFile = File(...),
notifier: Notifier = Depends(get_notifier),
user_data: User = Depends(get_current_admin_user)):
try:
file_path = f"temp_{file.filename}"
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
await notifier.send_photo(file_path, settings.CHAT_ID, caption=caption)
os.remove(file_path)
return {"status": "success", "message": "Фото отправлено"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Обработка:
caption: str = None
: подпись к фото.file: UploadFile = File(...)
: загружаемый файл.Временно сохраняет фото, отправляет его в Telegram, удаляет файл и возвращает ответ об успешной отправке или ошибке.
Метод для отправки документов
@router.post('/send_document')
async def send_document(caption: str = None,
file: UploadFile = File(...),
notifier: Notifier = Depends(get_notifier),
user_data: User = Depends(get_current_admin_user)):
try:
file_path = f"temp_{file.filename}"
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
await notifier.send_document(file_path, settings.CHAT_ID, caption=caption)
os.remove(file_path)
return {"status": "success", "message": "Документ отправлен"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Этот метод отправляет документ в Telegram аналогично отправке фото.
Регистрация роутера
Для завершения интеграции добавьте в app/main.py
регистрацию роутера и перезагрузите приложение:
from app.api.router import router as router_api
app.include_router(router_api)
Проверим в документации:
Перед продолжением обязательно протестируйте все методы на отправку сообщений в вашего бота. Тут должны выбрасываться ошибки если недостаточно прав и должно корректно отправляться сообщение с текстом и с медиа.
Если все работает корректно, то мы можем приступить к деплою FastApi приложения на сервис Amvera Cloud.
Деплой FastAPI приложения на Amvera Cloud
Перед началом деплоя в корне приложения необходимо создать файл настроек для Amvera — amvera.yml
. В этом файле указываются параметры деплоя приложения на платформу Amvera, включая среду, зависимости и команду для запуска. Вот пример полного содержимого файла:
meta:
environment: python
toolchain:
name: pip
version: 3.12
build:
requirementsPath: requirements.txt
run:
persistenceMount: /data
containerPort: 8000
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
Пошаговый процесс деплоя
Регистрация: Зарегистрируйтесь на сайте Amvera Cloud или выполните вход, если у вас уже есть аккаунт. Новым пользователям начисляется 111 рублей на основной баланс.
-
Создание проекта:
Перейдите в раздел проектов и нажмите «Создать проект».
Укажите название проекта, выберите подходящий тарифный план и нажмите «Далее».
-
Загрузка проекта:
-
На следующем шаге выберите способ загрузки проекта:
Загрузка через интерфейс: выберите эту опцию и загрузите файлы проекта.
Через Git: выберите этот вариант для работы с Git. Amvera предоставит необходимые команды для загрузки через Git.
После загрузки файлов нажмите «Далее».
-
Проверка настроек: Убедитесь, что все параметры указаны верно, и нажмите «Завершить» для завершения создания проекта.
Активация домена и пересборка проекта
После завершения деплоя нужно активировать бесплатный домен и пересобрать проект:
Откройте проект и перейдите на вкладку «Настройки».
Выберите «Добавить доменное имя» и выберите опцию «Бесплатное доменное имя». Убедитесь, что выбрано https.
Нажмите «Пересобрать проект», чтобы Amvera корректно привязала домен к проекту. Подождите 2–3 минуты, пока проект не запустится.
Для проверки работы проекта перейдите в документацию, используя выделенный под проект домен. В моем случае ссылка на документацию после деплоя имеет следующий вид:
https://fletappfastapi-yakvenalex.amvera.io/docs
Вы можете посетить эту ссылку, если интересно ознакомиться с работающим API. Полный код API вы найдете в моем телеграмм канале "Легкий путь в Python".
Выводы по первой части статьи
Подводя итоги первой части, можно сказать, что мы уже проделали значительную часть пути. У нас есть полноценный API, который решает задачи регистрации, авторизации, аутентификации и управляет доступом в зависимости от ролей. Дополнительно он включает методы для взаимодействия с Telegram API.
С деплоем этого приложения наш API стал доступен для интеграции с любыми системами, будь то веб-интерфейсы, мобильные приложения или десктопные приложения. Теперь его можно использовать из любой точки мира, имея лишь подключение к сети.
То есть, сейчас можно написать код Flet, трансформировать его в Desktop, сайт или мобильное устройство а далее, у всех кто будет пользоваться вашим Flet-приложением будет доступ к функциональности API. Считаю, что это уже не плохо.
Теперь переходим к разработке Flet-приложения.
Пишем Flet-приложение
Так как цель этой статьи — не обучить писать на Flet, а показать подходы и практику написания кода, акцент будет сделан на практический подход. Поэтому, для более полного понимания принципов этого фреймворка, желательно, чтобы вы ознакомились с его официальной документацией.
Итак, начнем.
Создаем проект в любимом IDE. Я, как обычно, буду использовать PyCharm.
В корне проекта создаем файл requirements.txt
и заполняем его следующим образом:
flet==0.24.1 # Библиотека для создания интерфейсов (UI) кроссплатформенных приложений на Python.
pyinstaller==6.11.1 # Утилита для упаковки Python-приложений в исполняемые файлы (десктопные приложения).
aiohttp==3.10.10 # Библиотека для асинхронных HTTP-запросов, часто используется для работы с API.
pydantic-settings==2.6.1 # Библиотека для управления настройками и конфигурациями на основе Pydantic.
pillow==11.0.0 # Библиотека для работы с изображениями (обработка и изменение форматов изображений).
Flet: библиотека для создания интерфейсов (UI) кроссплатформенных приложений на Python.
Aiohttp: библиотека для асинхронных HTTP-запросов. Будем её использовать для взаимодействия с нашим API.
Pydantic-settings: библиотека для управления настройками и конфигурациями на основе Pydantic.
Pyinstaller: утилита для упаковки Python-приложений в исполняемые файлы (десктопные приложения).
Pillow: библиотека для работы с изображениями, необходимая для корректной работы PyInstaller.
Установим зависимости:
pip install -r requirements.txt
Теперь мы готовы к созданию проекта. Для этого в Flet предусмотрена отдельная команда:
flet create my_flet_app
Вводим её в терминале (вместо my_flet_app
можно ввести любое имя). Если всё прошло корректно, то в терминале вы должны увидеть сообщение об успешном создании проекта. В созданной папке проекта удалите файл requirements.txt
. Должна получиться такая структура проекта. Файлы .gitignore
и README.md
тоже удалите, если они вам не нужны.
Теперь, для проверки, что всё установилось корректно, можно выполнить команду:
flet run my_flet_app
Если вы видите соответствующий результат, то вы готовы к написанию кода Flet-приложения.
Давайте посмотрим на общую структуру проекта:
import flet as ft
def main(page: ft.Page):
page.add(ft.SafeArea(ft.Text("Hello, Flet!")))
ft.app(main)
В этом коде логика остаётся такой же, как и в Flutter: существует главная функция, которая запускает всё приложение. Код внутри функции main
определяет, что будет отображаться на странице.
Концепция Flet схожа с Flutter: виджеты могут содержать другие виджеты. Здесь мы используем SafeArea
для безопасного отображения текста, что гарантирует, что содержимое не перекроется системными элементами интерфейса.
Во Flet принято придерживаться ООП-подхода, что здесь уместно, так как виджеты по своей сути схожи с объектами ООП. Этой концепции мы будем придерживаться далее.
Несмотря на то, что всё будет на одной странице, для удобства разделим приложение на три основных класса:
класс для регистрации,
класс для авторизации,
класс основного приложения.
И перед тем как мы к этому перейдём, давайте организуем наш проект. На выходе структура должна получиться следующей.
Внутри создадим пакет app
, в который поместим файлы с классами нашего приложения, а также сопутствующие файлы, такие как файл с настройками и важными утилитами, которые будут использоваться в проекте.
Начнем с файла __init__.py.
import flet as ft
from .utils import show_snack_bar, send_login_request, send_registration_data
from .login_form import LoginForm
from .main_app import MainApp
from .register_form import RegistrationForm
__all__ = ['ft',
'show_snack_bar',
'MainApp',
'RegistrationForm',
'send_login_request',
'send_registration_data',
'LoginForm']
Такую запись я сделал для удобства будущих импортов по всему приложению. Теперь у нас появится возможность все импортировать с одного места.
Теперь разберемся с файлом настроек. Технически, в этот проект данный файл можно было не включать, но я оставил его просто чтоб показать как в рамках Flet можно работать с переменными окружения.
Вот полный код файла config.py:
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
BASE_URL: str
model_config = SettingsConfigDict(env_file=f"{BASE_DIR}/../.env")
# Получаем параметры для загрузки переменных среды
settings = Settings()
Суть в том чтоб достать с файла .env ссылку на наш API. Давайте добавим этот файл и поместим его в корень проекта на уровень с requirements.txt. У меня получилось так:
Теперь подготовим наши утилиты и перейдем к написанию кода приложения.
Наши утилиты будут предусматривать методы для взаимодействия с API (функции) и одну функцию Flet для демонстрации уведомлений в виде SnackBar.
Выполним импорты:
import aiohttp
import flet as ft
from .config import settings
Для запросов мы будем использовать aiohttp.
Выведем отдельной переменной ссылку на сайт. Просто чтоб писать меньше кода:
BASE_URL = settings.BASE_URL
Опишем первый метод для отправки регистрационных данных в наш API:
async def send_registration_data(data):
url = f"{BASE_URL}/auth/register/"
headers = {"Content-Type": "application/json"}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data, headers=headers) as response:
if response.status == 200:
return await response.json()
else:
error_message = await response.text()
raise Exception(f"Ошибка регистрации: {error_message}")
Тут нет ничего примечательного. Мы просто будем собирать данные от пользователя на странице Flet приложения, а после будем отправлять их в API.
Метод для логина пользователя будет иметь похожую логику:
async def send_login_request(email, password):
url = f"{BASE_URL}/auth/login/"
headers = {"Content-Type": "application/json"}
payload = {"email": email, "password": password}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload, headers=headers) as response:
if response.status == 200:
return await response.json()
else:
error_message = await response.text()
raise Exception(f"Ошибка входа: {error_message}")
Тут я записал немного по другому просто чтоб показать вам другой подход, когда со страницы Flet-приложения мы передаем не JSON (словарь), а отдельные переменные.
Более интересным представляется метод получения информации о пользователе со страницы. Это связано с тем, что для инициализации пользователя и определения его прав и привилегий потребуется JWT-токен.
async def get_user_info(user_token: str):
url = f"{BASE_URL}/auth/me/"
headers = {
"Cookie": f"users_access_token={user_token}",
"accept": "application/json",
}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
return await response.json()
Тут самая важная часть — это user_token. Напоминаю, что после авторизации в системе наш API возвращает JWT-токен. Далее, на стороне Flet нашей задачей будет сохранение этого токена и далее его извлечение.
Методы для отправки сообщений в телеграмм бота будут уже включать в себя, как JWT-токен для проверки есть ли у пользователя права на отправку, так и данные, которые отправляет пользователь.
Начнем с простого метода для отправки чистого текста.
async def send_text_message(user_token: str, text_message: str):
url = f"{BASE_URL}/api/send_text"
headers = {
"Cookie": f"users_access_token={user_token}",
"accept": "application/json",
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json={"text": text_message}) as response:
return await response.json()
Тут нам достаточно передать текст, который будет вводить пользователь и его JWT-токен. Далее, если у этого пользователя будут права на отправку — она произойдет.
Работа с медиа (файлы и фото) с Flet тоже не особо трудная и сейчас, на примере следующего метода вы это поймете.
async def send_photo_message(user_token: str, file_path: str, caption: str):
url = f"{BASE_URL}/api/send_photo"
headers = {
"Cookie": f"users_access_token={user_token}",
"accept": "application/json",
}
params = {"caption": caption}
async with aiohttp.ClientSession() as session:
with open(file_path, 'rb') as file:
form = aiohttp.FormData()
form.add_field(
'file',
file,
filename=file_path.split('/')[-1],
content_type='image/jpeg'
)
async with session.post(url, headers=headers, params=params, data=form) as response:
return await response.json()
Если не вдаваться в особенности отправки файлов на стороне aiohttp, то вы можете заметить, что со стороны Flet достаточно передать путь к файлу, после чего появится возможности выполнить отправку медиафайла. В этом нам поможет очень полезная технология Flet о которой мы поговорим далее.
Метод для отправки файла практически идентичен с предыдущим:
async def send_file_message(user_token: str, file_path: str, caption: str):
url = f"{BASE_URL}/api/send_document"
headers = {
"Cookie": f"users_access_token={user_token}",
"accept": "application/json",
}
params = {"caption": caption}
async with aiohttp.ClientSession() as session:
with open(file_path, 'rb') as file:
form = aiohttp.FormData()
form.add_field(
'file',
file,
filename=file_path.split('/')[-1],
content_type='multipart/form-data'
)
async with session.post(url, headers=headers, params=params, data=form) as response:
return await response.json()
Меняется только тип контента и эндпоинт FastApi на который будет выполнена отправка.
Теперь разберем последнюю утилиту. Она будет понятна даже если вы совсем не знакомы с Flet:
def show_snack_bar(page, message):
snack_bar = ft.SnackBar(content=ft.Text(message))
page.overlay.append(snack_bar)
snack_bar.open = True
page.update()
Этот код показывает снэк-бар (всплывающее уведомление) с сообщением на странице. Давайте разберем его шаг за шагом:
def show_snack_bar(page, message):
Определение функции: Создаем функцию show_snack_bar
, которая принимает два параметра:
page
: объект страницы, на которой будет отображаться снэк-бар.message
: текст сообщения, который нужно показать в снэк-баре.
snack_bar = ft.SnackBar(content=ft.Text(message))
Создание снэк-бара: Создаем объект SnackBar
, который будет отображать всплывающее уведомление.
content=ft.Text(message)
: устанавливаем текст уведомления с помощью переданногоmessage
.
page.overlay.append(snack_bar)
Добавление на страницу: Помещаем снэк-бар в overlay
страницы. overlay
— это специальное место, где можно добавлять элементы, которые будут показываться поверх других.
snack_bar.open = True
Открытие снэк-бара: Устанавливаем свойство open
в True
, чтобы сделать снэк-бар видимым на экране.
page.update()
Обновление страницы: Обновляем страницу, чтобы показать изменения, в данном случае — отображение снэк-бара.
Вы еще сильнее поймете этот механизм, когда мы приступим к описанию кода приложения.
Описание экрана регистрации
Теперь мы готовы к описанию первого экрана — регистрации пользователя (app/register_form.py
).
Начнем с импортов:
from . import show_snack_bar, ft, send_registration_data
Здесь нас будут интересовать основной объект Flet, утилита для регистрации пользователя и утилита для демонстрации SnackBar
.
Этот код создает класс RegistrationForm
, который отображает форму регистрации и обрабатывает ввод пользователя. Класс включает поля ввода для имени, фамилии, email, номера телефона, пароля и подтверждения пароля, а также методы для проверки данных, отправки их на сервер и отображения уведомлений.
Создание класса RegistrationForm
Теперь создадим класс RegisterForm
, который будет отображать форму регистрации пользователя и обрабатывать ввод данных пользователя.
Класс включает следующие поля для ввода:
Имя
Фамилия
Email
Номер телефона
Пароль
Подтверждение пароля
Кроме того, в этом классе мы опишем методы для проверки введенных данных, отправки их на сервер и отображения уведомлений пользователю.
Инициализация класса
class RegistrationForm:
def __init__(self, page, on_success):
self.page = page
self.on_success = on_success
self.name_field = ft.TextField(label="Имя", width=300)
self.last_name_field = ft.TextField(label="Фамилия", width=300)
self.email_field = ft.TextField(label="Email", width=300, keyboard_type=ft.KeyboardType.EMAIL)
self.phone_number_field = ft.TextField(label="Номер телефона", width=300, keyboard_type=ft.KeyboardType.PHONE)
self.password_field = ft.TextField(label="Пароль", width=300, password=True, can_reveal_password=True)
self.confirm_password_field = ft.TextField(label="Повторите пароль", width=300, password=True,
can_reveal_password=True)
Параметры:
page
: объект страницы, на которой будет отображаться форма.on_success
: метод, который будет вызван после успешной регистрации. Эту часть мы более подробно рассмотрим далее.
Поля ввода
В TextField
нас интересует параметр value
, который по умолчанию равен None
. value
будет содержать введенное пользователем значение. Зная это, мы можем написать метод для валидации данных.
Метод для валидации данных
def validate(self):
if len(self.name_field.value) < 3:
show_snack_bar(self.page, "Имя должно содержать минимум 3 символа")
return False
if len(self.last_name_field.value) < 3:
show_snack_bar(self.page, "Фамилия должна содержать минимум 3 символа")
return False
if "@" not in self.email_field.value or "." not in self.email_field.value:
show_snack_bar(self.page, "Введите корректный email")
return False
if not self.phone_number_field.value.startswith('+') or not self.phone_number_field.value[1:].isdigit():
show_snack_bar(self.page, "Введите корректный номер телефона в формате +1234567890")
return False
if len(self.password_field.value) < 5:
show_snack_bar(self.page, "Пароль должен содержать минимум 5 символов")
return False
if self.password_field.value != self.confirm_password_field.value:
show_snack_bar(self.page, "Пароли не совпадают!")
return False
return True
Отображение полей на экране
async def display(self, e):
self.page.clean()
self.page.add(
ft.Column(
[
ft.Text("Регистрация", size=24, weight=ft.FontWeight.BOLD, color="#333"),
self.name_field,
self.last_name_field,
self.email_field,
self.phone_number_field,
self.password_field,
self.confirm_password_field,
ft.ElevatedButton(
text="Зарегистрироваться",
width=300,
bgcolor="#6200EE",
color="white",
on_click=self.on_register,
),
ft.TextButton("Уже есть аккаунт? Войти", on_click=self.on_success)
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
spacing=10,
)
)
Параметр e в display
e
нужен для соответствия требованиям Flet, ожидающего объект события. Хотя в данном случае параметр e
не используется, он должен быть включен в сигнатуру метода, чтобы избежать ошибок.
Методы для регистрации
Теперь добавим методы, которые будут непосредственно связаны с процессом регистрации.
async def on_register(self, e):
if self.validate():
user_data = {
"first_name": self.name_field.value,
"last_name": self.last_name_field.value,
"email": self.email_field.value,
"phone_number": self.phone_number_field.value,
"password": self.password_field.value,
"confirm_password": self.confirm_password_field.value,
}
await self.process_registration(user_data)
async def process_registration(self, user_data):
try:
response = await send_registration_data(user_data)
show_snack_bar(self.page, response.get("message", "Регистрация успешно завершена!"))
self.on_success()
except Exception as ex:
show_snack_bar(self.page, str(ex))
Описание методов
on_register
сначала выполняет валидацию данных. Если валидация успешна, формируем словарьuser_data
, забирая введенную информацию черезvalue
.process_registration
отправляет данные на сервер через утилитуsend_registration_data
.
Остальные методы не требуют особого внимания, так как общая идея вам уже понятна. Мы просто перемещаем более мелкие виджеты в более крупные и привязываем смену дисплеев к определённым событиям.
Конечный полный код этого класса имеет следующий вид:
Скрытый текст
from . import show_snack_bar, ft, send_registration_data
class RegistrationForm:
def __init__(self, page, on_success):
self.page = page
self.on_success = on_success
self.name_field = ft.TextField(label="Имя", width=300)
self.last_name_field = ft.TextField(label="Фамилия", width=300)
self.email_field = ft.TextField(label="Email", width=300, keyboard_type=ft.KeyboardType.EMAIL)
self.phone_number_field = ft.TextField(label="Номер телефона", width=300, keyboard_type=ft.KeyboardType.PHONE)
self.password_field = ft.TextField(label="Пароль", width=300, password=True, can_reveal_password=True)
self.confirm_password_field = ft.TextField(label="Повторите пароль", width=300, password=True,
can_reveal_password=True)
def validate(self):
if len(self.name_field.value) < 3:
show_snack_bar(self.page, "Имя должно содержать минимум 3 символа")
return False
if len(self.last_name_field.value) < 3:
show_snack_bar(self.page, "Фамилия должна содержать минимум 3 символа")
return False
if "@" not in self.email_field.value or "." not in self.email_field.value:
show_snack_bar(self.page, "Введите корректный email")
return False
if not self.phone_number_field.value.startswith('+') or not self.phone_number_field.value[1:].isdigit():
show_snack_bar(self.page, "Введите корректный номер телефона в формате +1234567890")
return False
if len(self.password_field.value) < 5:
show_snack_bar(self.page, "Пароль должен содержать минимум 5 символов")
return False
if self.password_field.value != self.confirm_password_field.value:
show_snack_bar(self.page, "Пароли не совпадают!")
return False
return True
async def on_register(self, e):
if self.validate():
user_data = {
"first_name": self.name_field.value,
"last_name": self.last_name_field.value,
"email": self.email_field.value,
"phone_number": self.phone_number_field.value,
"password": self.password_field.value,
"confirm_password": self.confirm_password_field.value,
}
await self.process_registration(user_data)
async def process_registration(self, user_data):
try:
response = await send_registration_data(user_data)
show_snack_bar(self.page, response.get("message", "Регистрация успешно завершена!"))
self.on_success()
except Exception as ex:
show_snack_bar(self.page, str(ex))
async def display(self, e):
self.page.clean()
self.page.add(
ft.Column(
[
ft.Text("Регистрация", size=24, weight=ft.FontWeight.BOLD, color="#333"),
self.name_field,
self.last_name_field,
self.email_field,
self.phone_number_field,
self.password_field,
self.confirm_password_field,
ft.ElevatedButton(
text="Зарегистрироваться",
width=300,
bgcolor="#6200EE",
color="white",
on_click=self.on_register, # Обратите внимание, что `on_register` теперь асинхронный
),
ft.TextButton("Уже есть аккаунт? Войти", on_click=self.on_success)
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
spacing=10,
)
)
Экран авторизации пользователей
Теперь реализуем класс, который позволит создать экран для входа пользователя в систему. Так как логика во многом похожа на экран регистрации, далее разберем только новые элементы на примере полного кода класса.
Скрытый текст
from . import show_snack_bar, ft, send_login_request
class LoginForm:
def __init__(self, page, on_success, on_switch_to_register):
self.page = page
self.on_success = on_success
self.on_switch_to_register = on_switch_to_register
self.email_field = ft.TextField(
label="Email",
width=300,
keyboard_type=ft.KeyboardType.EMAIL,
value='user@example.com',
)
self.password_field = ft.TextField(
label="Пароль",
width=300,
password=True,
can_reveal_password=True,
value='string'
)
async def on_login(self, e):
if not self.email_field.value or not self.password_field.value:
show_snack_bar(self.page, "Заполните все поля для входа")
return
await self.process_login(self.email_field.value, self.password_field.value)
async def process_login(self, email, password):
try:
response = await send_login_request(email, password)
if response.get("ok"):
access_token = response.get("access_token")
if access_token:
self.page.session.set("access_token", access_token)
show_snack_bar(self.page, response.get("message", "Вход успешно выполнен!"))
self.clear_fields()
await self.on_success()
else:
show_snack_bar(self.page, "Токен доступа не получен.")
else:
show_snack_bar(self.page, response.get("message", "Ошибка авторизации."))
except Exception as ex:
show_snack_bar(self.page, str(ex))
def clear_fields(self):
self.email_field.value = ""
self.password_field.value = ""
self.page.update()
async def display(self, e):
self.page.clean()
self.page.add(
ft.Column(
[
ft.Text("Вход", size=24, weight=ft.FontWeight.BOLD, color="#333"),
self.email_field,
self.password_field,
ft.ElevatedButton(
text="Войти",
width=300,
bgcolor="#6200EE",
color="white",
on_click=self.on_login, # Назначаем асинхронный метод напрямую
),
ft.TextButton(
"Нет аккаунта? Зарегистрироваться",
on_click=self.on_switch_to_register
)
],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
spacing=10,
)
)
Важная часть: Сохранение токена доступа пользователя
Обратим внимание на метод process_login
, который сохраняет токен пользователя в сессии:
async def process_login(self, email, password):
try:
response = await send_login_request(email, password)
if response.get("ok"):
access_token = response.get("access_token")
if access_token:
self.page.session.set("access_token", access_token)
show_snack_bar(self.page, response.get("message", "Вход успешно выполнен!"))
self.clear_fields()
await self.on_success()
else:
show_snack_bar(self.page, "Токен доступа не получен.")
else:
show_snack_bar(self.page, response.get("message", "Ошибка авторизации."))
except Exception as ex:
show_snack_bar(self.page, str(ex))
Метод process_login
выполняет асинхронную обработку логина, отправляя запрос на сервер и обрабатывая ответ.
После отправки запроса мы проверяем, успешен ли ответ.
-
Если успешен, пытаемся извлечь
access_token
из ответа сервера.Если токен отсутствует, показываем сообщение об ошибке.
Если токен получен, сохраняем его в сессии страницы с ключом
"access_token"
. Это позволяет приложению помнить, что пользователь авторизован, и использовать токен для последующих запросов.
Хранилище сессии удобно, так как оно работает в любом окружении: десктоп, веб и т.д.
Общая логика
Остальные элементы, такие как отображение полей ввода и кнопок, аналогичны классу регистрации.
Функциональная часть приложения (main_app.py)
Теперь нам осталось реализовать последний класс нашего приложения, который будет отвечать за инструменты для отправки сообщений. Кроме того, на этом экране будет реализована функциональность выхода из системы и проверка, доступен ли функционал приложения конкретному пользователю.
Выполним импорты:
import os
from . import ft, show_snack_bar
from .utils import get_user_info, send_text_message, send_photo_message, send_file_message
Теперь пропишем класс с инициализацией и разберемся в том, что написали.
class MainApp:
def __init__(self, page: ft.Page, on_switch_to_login):
self.page = page
self.page.title = "Bot Interaction App"
self.page.theme_mode = ft.ThemeMode.DARK
self.page.padding = 20
self.page.spacing = 20
self.content = ft.Column(spacing=20, expand=True, alignment=ft.MainAxisAlignment.CENTER)
# Поля для ввода данных
self.msg_input_block = ft.TextField(label="Введите сообщение", multiline=True, min_lines=3)
self.photo_caption = ft.TextField(label="Введите подпись к фото", multiline=True, min_lines=3)
self.file_note = ft.TextField(label="Примечание к файлу", multiline=True, min_lines=3)
# Выбранные файлы
self.selected_photo = None
self.selected_file = None
self.on_switch_to_login = on_switch_to_login
# Файловые выборщики для фото и файла
self.photo_picker = ft.FilePicker(on_result=self.on_file_picked)
self.file_picker = ft.FilePicker(on_result=self.on_file_picked)
self.page.overlay.extend([self.file_picker, self.photo_picker])
Инициализация начинается с методов для стилизации страницы. Далее идут поля ввода, которые мы разобрали ранее.
Подробнее разберем новый функционал.
На странице добавлены две переменные под выбранное фото и выбранный файл. Эти атрибуты (selected_photo
и selected_file
) будут хранить путь к выбранным файлу и фотографии. Изначально они равны None
, так как файлы еще не выбраны.
Функция переключения на логин: сохраняем функцию on_switch_to_login
в атрибуте self.on_switch_to_login
, чтобы использовать её при необходимости переключиться на экран логина.
Отдельно рассмотрим файловые сборщики:
self.photo_picker = ft.FilePicker(on_result=self.on_file_picked)
self.file_picker = ft.FilePicker(on_result=self.on_file_picked)
Файловые сборщики (или FilePickers
в Flet) предоставляют интерфейс для выбора файлов с устройства пользователя. Эти компоненты позволяют пользователю выбрать файл, который затем можно отправить на сервер или использовать в приложении.
Параметр on_result=self.on_file_picked
указывает, что когда пользователь выберет файл, результат (информация о выбранном файле) будет передан в функцию on_file_picked
. Этот метод обрабатывает результат и может, например, показать уведомление с именем файла или сохранить выбранный файл для дальнейшего использования.
Когда файл выбран, on_file_picked
обрабатывает событие выбора. Например, если пользователь выбрал изображение, его путь и имя сохраняются для последующей отправки. Если файл не выбран, можно показать уведомление.
page.overlay.extend([self.file_picker, self.photo_picker])
page.overlay
— это особый слой интерфейса, который позволяет добавлять элементы поверх основного контента страницы. Этот слой полезен для отображения элементов, таких как всплывающие окна или модальные диалоги, которые временно перекрывают основной интерфейс.extend([self.file_picker, self.photo_picker])
добавляет выборщики файлов (file_picker
иphoto_picker
) вoverlay
. Это позволяет выборщикам работать как всплывающие окна или модальные окна, поверх основного интерфейса страницы.
Теперь опишем метод для отображения экрана:
async def display(self, e=None):
self.page.clean()
access_token = self.page.session.get("access_token")
if not access_token:
self.show_login_required()
return
user_info = await get_user_info(access_token)
if user_info.get("role_id") < 3:
self.show_unauthorized(user_info)
else:
self.show_authorized(user_info)
После очищения страницы мы пытаемся получить токен пользователя из сессии. Если он не найден, вызываем функцию show_login_required()
.
def show_login_required(self):
# Сообщение о необходимости авторизации
self.content.controls = [
ft.Card(
content=ft.Container(
content=ft.Column([
ft.Text("Требуется авторизация", size=24, weight=ft.FontWeight.BOLD, color=ft.colors.RED_600),
ft.Text("Пожалуйста, войдите в систему, чтобы получить доступ к приложению.", size=16),
ft.ElevatedButton(
"Войти",
icon=ft.icons.LOGIN,
on_click=self.on_switch_to_login
)
], alignment=ft.MainAxisAlignment.CENTER, spacing=10),
padding=20
)
)
]
# Обновляем страницу для отображения изменений
self.page.add(self.content)
Здесь мы показываем сообщение о необходимости авторизации.
Если токен получен, выполняем метод для получения информации о пользователе (описан в утилитах).
Далее — простая проверка: если роль пользователя меньше 3, то у него нет прав для использования функционала приложения. В таком случае показываем следующее сообщение:
def show_unauthorized(self, user_info):
self.content.controls = [
ft.Card(
content=ft.Container(
content=ft.Column([
ft.Text(f"Привет, {user_info.get('first_name')} {user_info.get('last_name')}!",
size=24, weight=ft.FontWeight.BOLD),
ft.Text("К сожалению, у вас нет доступа к функционалу этого приложения.",
size=16, color=ft.colors.RED_400)
], alignment=ft.MainAxisAlignment.CENTER, spacing=10),
padding=20,
)
)
]
self.page.add(self.content)
self.page.add(self.show_exit_button())
Если прав достаточно, выводим главный экран приложения:
def show_authorized(self, user_info):
self.content.controls = [
ft.Card(
content=ft.Container(
content=ft.Column([
ft.Text(f"Добро пожаловать, {user_info.get('first_name')} {user_info.get('last_name')}!",
size=24, weight=ft.FontWeight.BOLD),
ft.Text("Выберите действие:", size=18),
ft.Row([
ft.ElevatedButton(
"Отправить сообщение",
icon=ft.icons.MESSAGE,
on_click=self.show_message_input
),
ft.ElevatedButton(
"Отправить фото",
icon=ft.icons.PHOTO_CAMERA,
on_click=self.show_photo_input
),
ft.ElevatedButton(
"Отправить файл",
icon=ft.icons.ATTACH_FILE,
on_click=self.show_file_input
),
], alignment=ft.MainAxisAlignment.CENTER, spacing=10),
], spacing=20, alignment=ft.MainAxisAlignment.CENTER),
padding=20
)
),
self.show_exit_button()
]
self.page.add(self.content)
Обратите внимание: в обоих случаях мы добавляем кнопку для выхода из системы, которая запускает метод LOGOUT
:
def show_exit_button(self):
return ft.Container(
content=ft.ElevatedButton(
"Выйти",
height=80,
width=250,
icon=ft.icons.EXIT_TO_APP,
style=ft.ButtonStyle(
bgcolor=ft.colors.RED_600,
color=ft.colors.WHITE,
shape=ft.RoundedRectangleBorder(radius=10),
elevation=5,
),
on_click=self.exit_app
),
alignment=ft.alignment.center,
expand=True
)
async def exit_app(self, e):
self.page.session.clear()
await self.on_switch_to_login(e)
Сейчас рассмотрим подробно метод для отправки файлов в бота. Кому будет интересно ознакомиться с полным кодом приложения и, если будут вопросы — переходите в мой телеграмм канал «Легкий путь в Python».
Начнем с экрана для отображения полей ввода описания к файлу и и выбора самого файла:
def show_file_input(self, e):
self.content.controls = [
ft.Card(
content=ft.Container(
content=ft.Column([
ft.Text("Отправка файла боту", size=20, weight=ft.FontWeight.BOLD),
self.file_note,
ft.ElevatedButton(
"Выбрать файл",
icon=ft.icons.UPLOAD_FILE,
on_click=lambda _: self.file_picker.pick_files(allow_multiple=False,
file_type=ft.FilePickerFileType.ANY)
),
ft.ElevatedButton("Отправить", icon=ft.icons.SEND, on_click=self.send_file_to_bot),
ft.ElevatedButton("Назад", icon=ft.icons.ARROW_BACK, on_click=self.display)
], spacing=20),
padding=20
)
)
]
self.page.update()
В целом, все те же поля ввода. Единственное что для красоты использовал виджет Card.
Начнем с выбора файла.
on_click=lambda _: self.file_picker.pick_files(allow_multiple=False,
file_type=ft.FilePickerFileType.ANY)
Лямбда-функция здесь нужна, чтобы вызвать pick_files при клике на кнопку без лишнего метода, и передать параметры, необходимые для ограничения выбора (только один файл любого типа). Этот подход делает код более компактным и сосредотачивает логику выбора файлов рядом с кнопкой, которая его вызывает.
Методы для отправки файлов и возврата на предыдущий экран описал уже в привычном виде.
Метод для отправки файла с подписью в бота:
async def send_file_to_bot(self, e):
if not self.selected_file:
show_snack_bar(self.page, "Пожалуйста, выберите файл для отправки")
return
access_token = self.page.session.get("access_token")
note = self.file_note.value or 'Файл без примечания'
file_path = self.selected_file.path
await send_file_message(access_token, file_path, note)
show_snack_bar(self.page, "Файл успешно отправлен!")
Тут идет уже знакомая для вас логика. Мы извлекаем токен из сессии, извлекаем примечание из поля примечания и путь к файлу с переменной select_file.patch. Далее, полученные данные мы передаем в метод отправки файлов, который мы описали в утилите.
Как путь к файлу оказался в select_file.path?
Все просто.
def on_file_picked(self, e: ft.FilePickerResultEvent):
if e.files:
selected_file = e.files[0]
file_name = selected_file.name
file_extension = os.path.splitext(file_name)[1].lower()
if file_extension in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
self.selected_photo = selected_file
file_type = "Фото"
else:
self.selected_file = selected_file
file_type = "Файл"
show_snack_bar(self.page, f"Выбран {file_type}: {file_name}")
else:
self.selected_photo = None
self.selected_file = None
show_snack_bar(self.page, "Медиа файл не выбран")
Метод on_file_picked обрабатывает событие выбора файла, которое возникает, когда пользователь завершает выбор файла в интерфейсе File Picker. Этот метод вызывается автоматически, так как он был зарегистрирован как обработчик события для FilePicker при инициализации self.photo_picker и self.file_picker.
Он проверяет, выбрал ли пользователь файл, и если да, то сохраняет информацию о выбранном файле и определяет его тип (фото или другой файл).
В конце метод показывает уведомление с названием выбранного файла или уведомляет пользователя, если файл не был выбран.
Этот метод помогает приложению корректно обрабатывать файлы, которые пользователь выбирает для отправки, и уведомляет его о результате выбора.
Теперь опишем main-файл приложения и посмотрим что у нас получилось.
Скрытый текст
from app import ft, LoginForm, MainApp, RegistrationForm
async def main(page: ft.Page):
page.title = "Одностраничное приложение"
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.padding = 20
# Определение стильной тёмной темы
dark_theme = ft.Theme(
color_scheme=ft.ColorScheme(
primary=ft.colors.INDIGO_400,
on_primary=ft.colors.WHITE,
primary_container=ft.colors.INDIGO_700,
on_primary_container=ft.colors.INDIGO_50,
secondary=ft.colors.TEAL_300,
on_secondary=ft.colors.BLACK,
background=ft.colors.GREY_900,
on_background=ft.colors.GREY_100,
surface=ft.colors.GREY_900,
on_surface=ft.colors.GREY_100,
error=ft.colors.RED_400,
on_error=ft.colors.WHITE,
surface_tint=ft.colors.INDIGO_400,
shadow=ft.colors.BLACK54,
),
font_family="Inter",
use_material3=True,
)
page.theme = dark_theme
page.update()
async def on_register_success(e):
await login_form.display(e)
# Объявляем экраны
main_app = MainApp(page, on_switch_to_login=on_register_success)
# Асинхронные коллбэки для переключения экранов
async def on_login_success():
await main_app.display()
async def on_switch_to_register(e):
await registration_form.display(e)
login_form = LoginForm(page, on_success=on_login_success, on_switch_to_register=on_switch_to_register)
registration_form = RegistrationForm(page, on_success=on_register_success)
await login_form.display(e=None) # Стартуем с формы входа
# Запускаем асинхронное приложение
ft.app(target=main)
В этом коде создается одностраничное приложение с темной темой, которое начинается с экрана входа и позволяет переключаться между экранами входа, регистрации и основной частью приложения.
Основная часть кода посвящена настройке темы и конфигурации экранов. При вынесении описания темы приложения за пределы кода, остаются только регистрация классов экранов и базовый функционал для отображения. Приложение включает три ключевых класса для экранов: LoginForm, RegistrationForm и MainApp, которые обеспечивают функционал входа, регистрации и работы с основным интерфейсом.
Код регистрирует классы, назначает обработчики для переключения между экранами, и затем запускает приложение, начав с экрана входа.
Выполним запуск уже знакомой нам командой:
flet run my_flet_app
Приложение запустилось в Desktop версии, что идет по умолчанию.
При запуске у меня сразу заполнены поля почты и пароля. Происходит это по следующей причине:
self.email_field = ft.TextField(
label="Email",
width=300,
keyboard_type=ft.KeyboardType.EMAIL,
value='user@example.com',
)
self.password_field = ft.TextField(
label="Пароль",
width=300,
password=True,
can_reveal_password=True,
value='string'
)
При инициализации мы явно заполнили value.
Попробуем кликнуть по кнопке «Нет аккаунта».
Переход на форму регистрации произошел корректный. Давайте выполним регистрацию.
Напоминаю, что по умолчанию у пользователя будет роль 1. То есть прав для пользования функционалом приложения у него не будет.
Обратите внимание. Поля с паролем автоматически закрыты. Это с коробки Flet.
SnackBar не подвел. Попробуем теперь выполнить вход.
Как и ожидалось прав пользования у меня нет.
Теперь выполню авторизацию с правами пользователя.
Теперь попробуем выполнить отправку файла в бота. Просто потому-что мы подробно разобрали этот код.
Отправлю текстовый документ со статьей, которую вы читаете.
Все работает, а значит что проект готов!
Трансформация в WEB-APP и деплой
Как я писал в начале статьи — трансформация Flet приложения в WEB-версию самое простое что есть во Flet.
Для того чтоб проект работал, как WEB-версия достаточно выполнить команду:
flet run --web --port PORT NAME_APP
В моем случае команда может иметь такой вид:
flet run --web --port 8800 my_flet_app
После выполнения этой команды мы запустили наш собственный веб-сервер, а код, который мы писали на языке программирования Python, был преобразован в код, включающий HTML, CSS и JavaScript.
Для того чтоб убедиться что это действительно полноценный веб-сайт я выполню его деполой на сервис Amvera. Логика та-же:
1. Готовим файл с настройками
2. Выполняем доставку файлов приложения на сервис Amvera
3. Активируем бесплатный домен
4. Пересобираем проектах
Шаги теже что на этапе деплоя FastApi приложения.
Файл с настройками (amvera.yml) тут может иметь следующий вид:
meta:
environment: python
toolchain:
name: pip
version: 3.12
build:
requirementsPath: requirements.txt
run:
persistenceMount: /data
containerPort: 8800
command: flet run --web --port 8800 my_flet_app
Изменился порт и команда запуска приложения. Файл amvera.yml ложим на один уровень с файлом requirements.txt.
Далее создаем проект в Amvera и доставляем в него файлы. Я снова буду использовать интерфейс загрузки файлов на сайте Amvera.
После доставки захожу в проект. Там переключаюсь в настройки и активирую бесплатный домен.
Теперь остается подождать пока станет активным приложение по выделенной ссылке. У меня процесс разворачивания и запуска контейнера занял около 3 минут.
В случае если у вас не подвяжется ссылка — выполните пересборку проекта. Для этого нажмите на эту кнопку:
В моем случае пересборка не потребовалась и сайт сразу стал доступен по адресу: https://fletfrontproject-yakvenalex.amvera.io/
Попробуем отправить текст с сайта:
Проверим
В веб-приложениях путь к файлу не отображается в браузере на стороне клиента по соображениям безопасности. В отличие от десктопных и мобильных приложений, где путь к файлу доступен, в веб-приложениях он скрыт. Вместо этого файлы передаются на сервер, где и обрабатываются.
В рамках этой статьи я не буду подробно рассматривать эту тему, так как для её реализации необходимо внести изменения в приложение Flet, в частности, запустить его через FastApi. Если у вас возникнут вопросы, я с удовольствием отвечу на них в следующих статьях или в своем Telegram-канале (в обсуждениях). В данный момент я не вижу в этом большого смысла.
Трансформация в Desktop приложение (легкий способ)
Сейчас я покажу легкий способ, который позволит получить Desktop версию из нашего приложения Flet (в моем случае это файл exe).
Мы уже установили Pyinstaller и Pillow, а значит нам достаточно теперь ввести 1 команду:
pyinstaller my_flet_app/main.py --noconsole --noconfirm --onefile --icon=my_flet_app/assets/icon.png --add-data ".env:." --clean
Команда собирает Python-приложение в один исполняемый файл с использованием PyInstaller.
my_flet_app/main.py
— основной файл приложения, находящийся в директории my_flet_app.--noconsole
— отключает консольное окно (для графических приложений на Windows).--noconfirm
— автоматически подтверждает перезапись предыдущих сборок.--onefile
— собирает все в один исполняемый файл.--icon=my_flet_app/assets/icon.png
— устанавливает иконку приложения из указанного файла.--add-data ".env:."
— добавляет .env файл в корень сборки.--clean
— удаляет временные файлы после завершения сборки.
Сборка, как правило, занимает не больше пары минут.
После завершения сборки изменится структура проекта. Исполняемый файл для запуска (в моем случае exe) вы найдете в созданной папке dist.
Перейдем в папку dist в проводнике и попробуем выполнить запуск приложения:
Полный код этого приложения с файлом exe, как и прочий свой контент, который я не публикую на Хабре — вы найдете в моем телеграмм канале «Легкий путь в Python».
Кроссплатформенность во Flet
Когда знакомишься с Flet, он производит приятное впечатление: простота написания кода, аккуратный дизайн и удобный процесс работы. Однако, при более глубоком анализе кроссплатформенность оставляет желать лучшего.
Основное преимущество Flutter, на котором базируется Flet, — это гибкость. Кроссплатформенность здесь подразумевает не только возможность запуска на разных устройствах, но и простоту разработки, особенно заметную при создании мобильных приложений. Разработка мобильного приложения на Flutter, например, для Android, выглядит так:
Открываете среду разработки (IDE).
Запускаете эмулятор Android.
Пишете код и благодаря функции «Hot reload» сразу видите изменения в эмуляторе.
Эта схема делает процесс создания мобильных интерфейсов быстрым и интуитивно понятным. Flutter, разработанный Google и активно развиваемый почти десять лет, предлагает удобную организацию кода и поддерживается языком программирования Dart, специально созданным для этой технологии. Dart прост в освоении, а Flutter — надежный выбор для кроссплатформенных приложений.
С Flet ситуация иная. При разработке для мобильных платформ приходится работать «вслепую», поскольку тестировать изменения можно только в веб- или десктоп-формате. После тестирования начинается процесс упаковки, который может занять десятки минут, и каждое изменение требует повторения этой длительной процедуры. В Flutter такие действия занимают секунды, тогда как в Flet это превращается в процесс, требующий терпения.
Таким образом, кроссплатформенность в Flet пока далека от идеала, мягко говоря. Кроме того, у Flet есть ещё один значительный недостаток: «под капотом» он содержит множество неявных и неочевидных зависимостей, что усложняет работу.
Часть с заморочками
Если вы решили создать полноценное десктопное или мобильное приложение на Flet, будьте готовы к дополнительным шагам и настройкам.
Установка Flutter
Для начала нужно установить Flutter SDK, который занимает около 3 ГБ на диске. Шаги следующие:
Перейдите на официальную страницу загрузки Flutter.
Скачайте версию, соответствующую вашей операционной системе.
Распакуйте архив в выбранную директорию.
Добавьте путь к папке
flutter/bin
в переменные окружения вашей системы, чтобы иметь доступ к командеflutter
из любого места.
Проверка установки
После установки откройте терминал и выполните команду:
flutter doctor
Эта команда проанализирует вашу систему и укажет на недостающие компоненты.
Дополнительные инструменты
В зависимости от вашей операционной системы и целевых платформ, flutter doctor
может порекомендовать установить следующие инструменты:
Android Studio: необходим для разработки и сборки Android-приложений.
Visual Studio: требуется для разработки под Windows. Убедитесь, что установлены необходимые рабочие нагрузки, такие как «Desktop development with C++».
Xcode: необходим для разработки под iOS и macOS (только для пользователей macOS).
Следуйте инструкциям flutter doctor
для установки и настройки этих инструментов.
Установка недостающих пакетов
В процессе настройки вам может потребоваться установить дополнительные пакеты и зависимости. flutter doctor
предоставит ссылки и инструкции для их установки.
После успешной установки всех необходимых инструментов и зависимостей вы готовы к сборке приложения для выбранных платформ. Имейте в виду, что процесс сборки может занять значительное время, особенно при первой компиляции.
Команды для сборки в Flet
Flet предусматривает следующие команды для упаковки приложения под определённую операционную систему.
Для Android:
flet build apk
Подробнее по командам запуска под другие платформы: Flet Publish
Выводы
После тщательного анализа Flet становится очевидно, что на текущем этапе развития эта технология имеет значительные ограничения. Основная заявленная функция — кроссплатформенность — реализована недостаточно эффективно. Процесс компиляции занимает продолжительное время, а адаптация под различные платформы сопряжена с трудностями.
Из положительных сторон можно отметить удобный синтаксис, однако Flet не предлагает принципиально новых решений. Фреймворк использует модель Flutter, заменяя язык программирования Dart на Python. Однако такая замена не всегда оправдана, так как Python не обеспечивает той же производительности и оптимизации, что и Dart, специально разработанный для Flutter.
Кроме того, Flet требует установки дополнительных инструментов, таких как Flutter SDK, Android Studio и Visual Studio, что усложняет процесс разработки и увеличивает время настройки среды. Также Flet имеет ограниченную поддержку сторонних пакетов и виджетов, что может ограничивать функциональность создаваемых приложений.
Таким образом, несмотря на некоторые преимущества, Flet в текущем состоянии не может конкурировать с более зрелыми и оптимизированными инструментами для кроссплатформенной разработки.
Если вас привлекает идея кроссплатформенной разработки, рекомендую обратить внимание на Dart и Flutter. Если у вас есть опыт в Python, JavaScript или C++, освоение Dart займет не более недели, так как его синтаксис схож с этими языками, а сама реализация интуитивно понятна.
Начав работать с чистым Flutter, вы получите доступ к мощному инструменту для создания высокопроизводительных кроссплатформенных приложений без ограничений, присущих менее зрелым решениям.
До скорого!
Буду рад видеть вас в своем Telegram‑канале «Легкий путь в Python» и надеюсь на положительный отклик по этой статье. Сбор материала потребовал значительных усилий, и ваша обратная связь будет ценна для дальнейшей работы.
Комментарии (10)
redfox0
14.11.2024 07:14Может, подумаете о включении сжатия на вашем хостинге? gzip, br там...
yakvenalex Автор
14.11.2024 07:14На платформе Amvera Cloud есть возможность работать через Git. Для отправки и обновления файлов можно использовать стандартные команды. Однако, чтобы сэкономить время, я часто загружаю все файлы напрямую через интерфейс на сайте. Это я к тому что как кому удобно)
redfox0
14.11.2024 07:14Вы даже не поняли, про что я говорю.
Про сжатие ответа веб-приложений. Сейчас этот клиентский файл весит 8 МБ: https://fletfrontproject-yakvenalex.amvera.io/main.dart.js - его можно легко сжать средствами хостинга при отдаче браузеру. И ещё как я вижу у вас стоит жёсткий rate-limit из-за чего файл скачивается со скоростью 100-200 КБ/с. Время скачивания файла можете посчитать сами.
o5boleg
14.11.2024 07:14Спасибо за статью! Видно, что вы вложили много времени и усилий в изучение нового инструмента. Flet действительно может быть полезен Python-разработчикам для создания простых мобильных приложений быстро и без лишних сложностей. Хочу немного уточнить ваше замечание о невозможности быстро протестировать Flet на мобильном устройстве без предварительной сборки - это можно сделать довольно просто. Достаточно установить мобильное приложение Flet, и тогда в нём можно тестировать ваш проект без предварительной компиляции. Подробности — здесь: https://flet.dev/docs/getting-started/testing-on-android
yakvenalex Автор
14.11.2024 07:14Благодарю за обратную связь. Тут да, возможно до конца не разобрался, но это не отменяет факта что он сырой и неудобен на фоне Flutter)
redfox0
переходы между экранами мне не нравятся. то ли у автора не очень получилось, то ли так принято. Увеличивается стек и объекты предыдущих экранов копятся.
веб-приложение весит 25,7 МБ (без сжатия и со шрифтами). Без шрифтов - около 8 МБ. Про потребление вкладкой 150 МБ ram и говорить как-то не хочется.
десктопное приложение потребляет 60 МБ ram (по скриншотам из статьи).
полностью исчезли аннотации из кода
yakvenalex Автор
Основная идея этой статьи заключается в том, что Flet не является готовым к использованию продуктом. Я не занимался оптимизацией, так как не планирую применять эту технологию на практике. Считаю, что фреймворк требует доработки и не может конкурировать с Flutter.