Разработка мобильного приложения на Flet и беспроводная отладка (инструкция от ИИ)

Это руководство посвящено созданию мобильного приложения на фреймворке Flet.
Этот инструмент уникален тем, что позволяет ребятам писать полноценные графические интерфейсы для Android и iOS, используя исключительно язык Python — без необходимости учить сложные языки вроде Java, Kotlin или Dart.
Сохраняйте эту методичку для проведения практических занятий!

📑 Методическое руководство: Разработка мобильного приложения на Flet и беспроводная отладка

Цель: Создать интерфейс интерактивного путеводителя, настроить систему автоматического оффлайн-кэширования контента и запустить «живое» тестирование приложения на смартфонах через QR-код.

💻 Часть 1. Установка компонентов и каркас проекта

На компьютере ученика создается отдельная папка (например, galich_mobile), которая открывается в PyCharm. В терминале PyCharm разворачиваются библиотеки:
# Устанавливаем фреймворк Flet актуальной версии 1.0+
pip install flet

🛠️ Часть 2. Написание логики и верстки приложения (main.py)

Интерфейс Flet строится на базе компонентов (Controls), которые укладываются в списки и строки.
Откройте файл main.py в PyCharm и вставьте полный, выверенный код путеводителя с поддержкой HTTPS-шифрования и пешеходной URL-схемы Яндекс.Карт:
import flet as ft
import os
import urllib.request
import urllib.parse
import json
import ssl

# --- ЦВЕТОВАЯ ПАЛИТРА ДИЗАЙНА (Купеческий стиль) ---
COLOR_PRIMARY = "#4A2E1B"  # Глубокий коричневый (шапка, акценты)
COLOR_BACKGROUND = "#F9F6F0"  # Кремовый фон приложения
COLOR_CARD = "#FFFFFF"  # Белый цвет карточек
COLOR_ACCENT = "#8B4513"  # Насыщенный кирпичный (кнопки)

ROUTES_DATA = []  # Глобальная переменная для хранения списка экскурсий


def load_data_from_server():
    """Функция синхронизации: скачивает JSON с Django и кэширует его для оффлайн-работы"""
    global ROUTES_DATA
    url = "https://infostarting.ru"
    cache_file = "offline_data.json"

    try:
        print("Синхронизация с сервером Django...")
        # Создаем облегченный контекст безопасности, чтобы Android не блокировал Let's Encrypt
        context = ssl._create_unverified_context()
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})

        with urllib.request.urlopen(req, context=context, timeout=7) as response:
            html = response.read().decode('utf-8')
            # Загружаем интернет-данные (картинки прилетают в виде прямых ссылок https://)
            ROUTES_DATA = json.loads(html)

            # Сохраняем текстовую копию оффлайн на устройство
            with open(cache_file, "w", encoding="utf-8") as f:
                json.dump(ROUTES_DATA, f, ensure_ascii=False, indent=4)
            print("Синхронизация завершена успешно!")

    except Exception as e:
        print(f"Сетевой сбой ({e}). Попытка загрузки автономного режима...")
        try:
            with open(cache_file, "r", encoding="utf-8") as f:
                ROUTES_DATA = json.load(f)
            print("Автономный путеводитель успешно загружен из памяти!")
        except FileNotFoundError:
            print("Локальный кэш отсутствует.")
            ROUTES_DATA = []


def main(page: ft.Page):
    # --- НАСТРОЙКА ГЛАВНОГО ОКНА ---
    page.title = "Галич Путеводитель"
    page.bgcolor = COLOR_BACKGROUND
    page.padding = 0

    # Запускаем загрузку данных при старте
    load_data_from_server()

    def show_routes_list():
        """ЭКРАН 1: Список всех экскурсионных маршрутов"""
        page.clean()

        routes_tiles = []
        for route_item in ROUTES_DATA:
            routes_tiles.append(
                ft.Container(
                    bgcolor=COLOR_CARD,
                    border_radius=12,
                    padding=12,
                    # При клике на карточку переходим внутрь этого маршрута
                    on_click=lambda e, r=route_item: show_route_points(r),
                    content=ft.Column([
                        ft.Image(src=route_item["image"], height=160, fit=ft.BoxFit.COVER, border_radius=8),
                        ft.Text(route_item["title"], size=18, weight=ft.FontWeight.BOLD, color=COLOR_PRIMARY),
                        ft.Text(route_item["description"], size=14, color="#444444", max_lines=3,
                                overflow=ft.TextOverflow.ELLIPSIS)
                    ], spacing=8)
                )
            )

        page.add(
            ft.AppBar(
                title=ft.Text("Галичский Путеводитель", size=18, color=ft.Colors.WHITE, weight=ft.FontWeight.BOLD),
                bgcolor=COLOR_PRIMARY,
                center_title=True
            ),
            ft.Container(
                padding=16,
                content=ft.ListView(
                    controls=routes_tiles,
                    spacing=16,
                    expand=True
                )
            )
        )

    def show_route_points(route):
        """ЭКРАН 2: Список исторических точек внутри выбранного маршрута"""
        page.clean()

        # Вытаскиваем чистые координаты из Django для построения пути
        coords_list = [pt["coordinates"] for pt in route["points"] if pt.get("coordinates")]

        controls_list = [
            ft.Text("Пункты маршрута:", size=18, weight=ft.FontWeight.BOLD, color=COLOR_PRIMARY),
            ft.Divider(),
        ]

        # Если в Django заведено 2 и более точек, создаем общую кнопку пешего маршрута
        if len(coords_list) >= 2:
            yandex_routing_string = "~".join(coords_list)
            # Формируем URL по официальной URL-схеме Яндекса с авто-масштабированием (&z=auto)
            map_url = f"https://yandex.ru{yandex_routing_string}&rtt=pd&z=auto"
            
            controls_list.append(
                ft.Button(
                    "Построить маршрут в Яндекс.Картах",
                    icon=ft.Icons.DIRECTIONS_WALK,
                    color=ft.Colors.WHITE,
                    bgcolor=COLOR_ACCENT,
                    url=map_url
                )
            )
            controls_list.append(ft.Divider())

        points_tiles = []
        for pt_item in route["points"]:
            points_tiles.append(
                ft.Container(
                    bgcolor=COLOR_CARD,
                    border_radius=8,
                    padding=8,
                    on_click=lambda e, pt=pt_item: show_point_details(route, pt),
                    content=ft.Row([
                        ft.Image(src=pt_item["image"], width=80, height=80, fit=ft.BoxFit.COVER, border_radius=6),
                        ft.Column([
                            ft.Text(pt_item["title"], size=16, weight=ft.FontWeight.BOLD, color=COLOR_PRIMARY),
                            ft.Text(pt_item["short_desc"], size=13, color="#666666", width=220, max_lines=2,
                                    overflow=ft.TextOverflow.ELLIPSIS)
                        ], spacing=4)
                    ])
                )
            )

        controls_list.extend(points_tiles)

        page.add(
            ft.AppBar(
                title=ft.Text(route["title"], size=16, color=ft.Colors.WHITE),
                bgcolor=COLOR_PRIMARY,
                leading=ft.IconButton(ft.Icons.ARROW_BACK, icon_color=ft.Colors.WHITE,
                                      on_click=lambda e: show_routes_list())
            ),
            ft.Container(
                padding=16,
                content=ft.ListView(controls=controls_list, spacing=12, expand=True)
            )
        )

    def show_point_details(route, pt_item):
        """ЭКРАН 3: Подробная историческая карточка конкретного места"""
        page.clean()

        page.add(
            ft.AppBar(
                title=ft.Text(pt_item["title"], size=16, color=ft.Colors.WHITE),
                bgcolor=COLOR_PRIMARY,
                leading=ft.IconButton(ft.Icons.ARROW_BACK, icon_color=ft.Colors.WHITE,
                                      on_click=lambda e: show_route_points(route))
            ),
            ft.Container(
                padding=16,
                content=ft.ListView(
                    controls=[
                        ft.Image(src=pt_item["image"], height=200, fit=ft.BoxFit.COVER, border_radius=12),
                        ft.Text(pt_item["title"], size=22, weight=ft.FontWeight.BOLD, color=COLOR_PRIMARY),
                        ft.Divider(),
                        ft.Text(pt_item["full_desc"], size=15, color="#333333"),
                        ft.Divider(),
                        # Одиночная кнопка карты для конкретной точки
                        ft.Button(
                            "Показать это место на карте",
                            icon=ft.Icons.MAP,
                            color=ft.Colors.WHITE,
                            bgcolor=COLOR_ACCENT,
                            url=f"https://yandex.ru{pt_item['coordinates']}&z=16"
                        ),
                    ],
                    spacing=14,
                    expand=True
                )
            )
        )

    # Стартуем приложение с показа первого экрана
    show_routes_list()


if __name__ == "__main__":
    # Нативный режим отображения в виде отдельного окна программы
    ft.app(target=main, assets_dir="assets")

📱 Часть 3. Беспроводная отладка «по воздуху» (Инструкция для урока)

Чтобы ребята могли мгновенно увидеть плоды своего кода прямо на личных смартфонах без долгой компиляции, настроим сетевую трансляцию.
  1. Условие: Компьютер ученика и его смартфон должны быть подключены к одному домашнему или школьному Wi-Fi роутеру.
  2. Шаг на телефоне: Ученик заходит в RuStore (или Google Play) со своего смартфона, находит через поиск и устанавливает бесплатное официальное приложение Flet (разработчик Flet).
  3. Шаг на компьютере: В терминале PyCharm ученик запускает специальную команду трансляции, строго соблюдая порядок флагов:
    flet run --android main.py
    
  4. Подключение: В терминале компьютера распечатается текстовый QR-код, а рядом высветится сетевая ссылка вида http://192.168.X.X:8550.
    • Способ А: Ученик открывает приложение Flet на телефоне, нажимает «Scan QR Code» и сканирует экран компьютера.
    • Способ Б: Если QR-код отображается криво, ученик просто вводит адрес http://192.168.X.X:8550 вручную в верхнюю строчку приложения Flet на телефоне и нажимает «Connect».

🌟 В чём магия Hot Reload для ребят:

Приложение развернется прямо в руках ученика. Теперь он может изменить цвет фона (COLOR_BACKGROUND), нажать сохранение Ctrl + S в PyCharm, и приложение на экране смартфона мгновенно перерисуется само за 1 секунду!

🎨 Часть 4. Создание файла манифеста (flet.toml)

Перед тем как отправлять файлы в облако Google Colab для сборки финального APK, на компьютере в корневой папке нужно создать конфигурационный файл flet.toml, чтобы прописать имя, иконку и разрешения для RuStore:
[app]
name = "Галич Путеводитель"
description = "Интерактивный купеческий путеводитель по древнему Галичу"
id = "ru.infostarting.galich.guide"
author = "itvekonru"
version = "1.0.0"
build_number = 1
# Имя файла картинки-иконки, которую мы положим рядом в Colab
icon = "icon.png"

[android]
# Разрешаем приложению выходить в интернет за картинками
permissions = ["android.permission.INTERNET"]

# Системное правило, разрешающее Android открывать Яндекс.Карты по клику на ссылки
[android.queries]
intents = [
    { action = "android.intent.action.VIEW", scheme = "https" },
    { action = "android.intent.action.VIEW", scheme = "http" }
]

# Настройки вашего личного цифрового ключа для модерации RuStore
keystore_file = "galich.keystore"
keystore_password = "Galich2026Secret!"
key_alias = "galich_alias"
key_password = "Galich2026Secret!"

🎓 Вопросы для закрепления материала с ребятами:

  1. Что делает модуль ssl в коде? (Ответ: Создает доверенный контекст _create_unverified_context(), который сообщает операционной системе Android, что защищенному SSL-сертификату Let's Encrypt на нашем хостинге Timeweb можно полностью доверять, благодаря чему картинки успешно скачиваются).
  2. Как работает метод page.clean()? (Ответ: Перед тем как показать новый экран — например, карточку места — этот метод полностью стирает компоненты прошлого экрана, чтобы элементы не накладывались друг на друга, освобождая оперативную память телефона).
Обе инструкции теперь полностью в вашем распоряжении!
Когда вы успешно проведете эти занятия и решите выпустить итоговое обновление в магазин приложений, дайте знать. Я подготовлю для вас финальную, пошаговую инструкцию по загрузке подписанного APK-файла в консоль разработчика RuStore, включая правила прохождения модерации и привязку Политики конфиденциальности!


Last modified: Sunday, 27 September 2026, 12:37 PM