Ask the assistant
0/400
Your question is processed by an AI service and kept to improve answers. Do not enter personal data.

    REEEL Architecture Best Practices

    This page has not been translated into English yet. The Russian original is below.

    Правила построения приложений на REEEL. Синтаксис описан в спеке формата ( packages/workspace/reeel_element_parser/docs/reeel_toon_format.md ) — здесь когда что выбирать . Правила визуальной вёрстки — в index.md . Ярусы: MUST — нарушение даёт баг или техдолг; SHOULD — дефолт стиля, отступай осознанно.

    0. Сигилы несут семантику (MUST)

    $name — чтение/подписка на переменную. @name на стороне значения — объявление выходной переменной, в которую пишет элемент. Это работает и для именованных ( data:@products ), и для позиционных аргументов ( MapValue $routePath @tab ). Направление потока данных всегда видно из сигилов — не из имён параметров.

    Поток данных

    1. Данные между экранами — только через маршрут (MUST)

    Идентичность — path-параметром, объект — вторым аргументом navigate , чтение — генератором RouteData . Путь собирается вложенным template string прямо в вызове:

    // main.reeel
    AppRoute "/detail/:id"
    
    // карточка
    ListTile onTap:(){navigator.show($"/detail/${item['id']}", item)}
    
    // detail.reeel
    RouteData id:@productId extra:@product
      Text {product['title'] ?? ""}

    Тем же генератором читается и то, КАКОЙ маршрут открыт, — этим оболочка подсвечивает активный пункт. Работает по обе стороны навигатора: и на экране маршрута, и в @Macros id:shell над ним. Путь статический ( /detail/:id → /detail ), поэтому таблица не зависит от значений параметров:

    @AppNavBar
      RouteData path:@routePath
        MapValue $routePath @tab "/home":0 "/catalog":1
          NavigationBar selectedIndex:$tab

    Переменная, которую файл не объявил, — всегда ошибка: если путь не прочитан RouteData , взяться ему неоткуда.

    Антипаттерн — глобальная Storage-переменная как «передатчик» ( onTap:(){selected_product=index; navigator.show("/detail")} ): скрытая связность, ломает deep link, требует повторного парсинга данных.

    2. extra эфемерален — без фолбэк-инфраструктуры (MUST)

    extra есть только при навигации внутри приложения. Экран читает поля как {product['field'] ?? ""} — на прямом заходе они пустые, и это норма. НЕ добавляй на экран деталей JsonData-фолбэк «достань по id, если extra нет»: экран не должен повторно загружать данные ради edge-кейса.

    3. Списки: данные → данные, не «скрой строку» (MUST)

    Фильтрация и трансформация — генераторами, список получает уже готовые данные:

    FilterData from:$products data:@filtered count:@filteredCount
      where:(item){isEmpty(query) || contains(lowerCase(item['title'] ?? ""), lowerCase(query))}
      ListView.builder itemCount:$filteredCount
        @Macros id:itemBuilder index:0
          $ProductCard item:{filtered[index]}

    Visibility visible:{...} внутри itemBuilder для фильтрации запрещён: ломает счётчики, скролл и состояния «пока пусто».

    4. Каждый сабскрипт на данных — с ?? (MUST)

    Выходы генераторов равны null , пока данные не загрузились; сабскрипты на null деградируют в null , а не падают. Поэтому каноническая форма — {products[index]['title'] ?? ""} ; голый сабскрипт в вёрстке — баг по построению.

    5. Карточке данных — объект целиком (SHOULD)

    $ProductCard item:{products[index]} — поля распаковываются внутри макроса ( {item['title'] ?? ""} ). Не протаскивай по пять параметров через каждый call site. Касается макросов-представлений данных; макросу-кнопке объект не нужен.

    Выражения

    6. Маппинг вместо тернарных цепочек, в обе стороны (SHOULD)

    Тернарник глубже одного уровня — сигнал, что нужен MapValue . Короткая форма — дефолт: первый позиционный — вход, второй — выход, остальные key:value — таблица, несовпавшее падает на ПЕРВУЮ запись:

    MapValue $routePath @tab "/home":0 "/catalog":1 "/favorites":2 "/profile":3

    Обратное направление (индекс → значение в колбэках) — pick() :

    onDestinationSelected:(index){navigator.show(pick(index, "/home", "/catalog", "/favorites", "/profile"))}

    Длинные таблицы выноси в #map на отдельной строке; именованная форма ( value: / to: / default: ) допустима, когда нужна явность — default: перекрывает фолбэк первой записи.

    7. Коллекции в Storage — литералы и иммутабельные операции (MUST)

    Динамические ключи в Storage невозможны — коллекция живёт в одном ключе. Объявляй литералом, мутируй функциями коллекций (каждая возвращает НОВУЮ коллекцию — присваивание нотифицирует подписчиков и персистит):

    Storage @favorites:[]
      IconButton onPressed:{favorites=toggle(favorites, productId)}
        Icon {has(favorites, productId) ? "favorite" : "favorite_border"}

    Скалярные элементы сравниваются по значению ( has / toggle / remove ); списки объектов — по ключу через *Where -варианты:

    Storage @cities:[]
      ListTile onTap:(){cities=add(cities, city)}
      IconButton onPressed:{cities=removeWhere(cities, "name", city['name'])}

    Живая переменная — настоящий List/Map (сабскрипты работают напрямую), в бекинг-хранилище она уходит JSON-строкой автоматически. Литералы: [a, b] — список, [key:value, …] — map, [] / [:] — пустые; литералы — для констант, $ref внутри не разворачивается.

    Тема и локализация

    Цвета — токенами $theme.* , не hex (SHOULD)

    color:$theme.primary , не color:#9B8AFB . Токены ambient (доступны в любом файле без объявления), следуют теме проекта/бренда и переключаются ( theme.set("dark") ). Hex-литерал допустим лишь там, где цвет действительно одноразовый и вне темы.

    Строки — ключами $strings.* , не литералами (SHOULD)

    Text $strings.welcome , аргументы/плюрал — {strings.t("greeting", name:$user)} . Строки живут в data/strings/<locale>.arb , язык переключается strings.set("ru") . Литералы — только для не-текста (символы, эмодзи) или временных заглушек.

    Ambient-чтения прикрывать ?? (MUST)

    Dotted-чтение неизвестного токена/ключа даёт null : {theme.accent ?? "#000000"} , {strings.title ?? "—"} . Не объявляй свои @theme / @strings — затенят ambient. Подробнее: Тема и локализация .

    Каркас приложения

    8. Роут = файл; пути с : — в кавычках (MUST)

    AppRoute "/detail/:id" резолвится в detail.reeel по статическим сегментам. Без кавычек : парсится как разделитель именованного параметра.

    9. Двойной Scaffold с фиксированными ролями (SHOULD)

    Внешний Scaffold — в shell-макросе AppMainBuilder : только персистентный chrome ( bottomNavigationBar , фон), не пересоздаётся при переходах. Внутренний — на каждом экране: его собственный AppBar . AppBar в shell не поднимать — пришлось бы реактивно перестраивать его по маршруту. backgroundColor дублируется на обоих уровнях осознанно: внутренний Scaffold без явного цвета закрасит фон темой Material.

    10. В шаблонах и примерах нет мёртвого UI (MUST для шаблонов)

    Каждый контрол делает что-то наблюдаемое. Декоративный «выбор темы» без эффекта хуже отсутствия контрола: шаблон — учебник, мёртвый контрол учит неправильному.

    11. Ссылка на файл — от корня проекта (MUST)

    // ✗ относительно файла — работает в редакторе, ломается после публикации
    Image src:"../shared/logo.png"
    
    // ✓ от корня проекта
    Image src:"data/images/logo.png"

    Путь от корня — единственная форма, о которой договорились все, кто его читает: загрузчик приложения, компилятор сайта, статический экспорт и манифест ревизии. «Текущий файл» такой точкой отсчёта быть не может: раскрытие макроса переносит разметку между файлами, и на момент резолва это уже не тот файл, где строка написана.

    Отдельно про .. : он не просто не рекомендуется — он не синхронизируется . И клиент, и сервер отклоняют путь с dot-сегментом, так что такая ссылка называет файл, которого в опубликованном проекте быть не может. При этом в редакторе он открывается с диска, поэтому ошибку видно только после публикации — и поэтому анализатор сообщает о ней как об error .

    Картинки живут в data/images/ (вложенность любая). Что уже есть в проекте, показывает reeel_assets — вместе с размерами и весом.

    12. Файл длиннее 250 строк или 12 КБ — делится на компоненты (SHOULD)

    Что наступит раньше. Самостоятельные куски — секция экрана, бит ролика — уходят в reeel/components/<name>.reeel как @ -макросы с import там, где используются; внешние переменные, которые кусок читает, передаются параметрами. Причина практическая: файл такого размера не переписать целиком в один ответ агента вместе с рассуждением, а каждая правка в нём — поиск по чужому коду. Анализатор предупреждает file_too_long ; исправление ( reeel_diagnostics {fix:true, file:"…"} — без file файл не режется) или reeel_extract_component выносят кусок, расставляют импорт и сообщают, что куда переехало.

    @Name на верхнем уровне файла объявляет макрос; @Macros index:0 — слот шаблона одного элемента *Repeat / itemBuilder , а не объявление. @Macros Ball … — ошибка ( macro_slot_as_declaration ), правильно @Ball … .

    Сцена motion держит в корне часы, *Layer in:/out: , звук и субтитры; каждый бит — свой макрос. JsonData — РОДИТЕЛЬ *Scene , не сосед: два корня превью показывает галереей.

    DOCS
    dark_mode_baseline
    RU/EN
    menu_baseline