Плагин MEL для Android Studio
Тул-окно «MEL» показывает MEL-экраны проекта как стейт-машины. Слева каталог экранов, справа на выбор: граф состояний, живое превью и отчёт о проблемах графа. Любой элемент открывает исходник. Вдобавок в редакторе работают 26 инспекций MEL.
О странице
Документация модуля :tools:mel-idea-plugin репозитория sbbol_android_mel
(id ru.sberbank_sbbol.mel.idea, версия 0.1.3, к библиотекам
ru.sberbank_sbbol.mel 0.2.0) для разработчика: как установить плагин, на каких студиях
он работает, что умеет сейчас и что запланировано. Раздел «умеет сейчас» сверен с кодом: того, чего
нет в коде, там тоже нет.
Источник истины по архитектуре MEL — код в :core:mel, затем
спека. Учебное введение — руководство,
правила detekt — mel-detekt.
Установка
Из репозитория плагинов
Плагин раздаётся не через JetBrains Marketplace, а из собственного репозитория плагинов. Это два
файла по фиксированным адресам: zip плагина и дескриптор updatePlugins.xml. Оба лежат в
папке mel-maven/mel-plugins репозитория sbbol_android_mel и раздаются как
raw-файлы git-хостинга. Студия опрашивает дескриптор, ставит и обновляет плагин как обычный.
- Settings → Plugins → ⚙ → Manage Plugin Repositories → +, добавить raw-адрес файла
updatePlugins.xmlиз папкиmel-maven/mel-pluginsрепозиторияsbbol_android_mel. Адрес — по месту. Репозиторий закрытый, поэтому при скачивании студия попросит аутентификацию. - Во вкладке Marketplace найти MEL, установить и перезапустить студию.
- Открыть тул-окно MEL (по умолчанию пристыковано справа).
mel-maven/mel-plugins в репозитории сейчас нет: плагин туда ещё не выкладывался.
Пока этого не сделали, ставьте zip с диска (см. ниже). Всё описанное на странице относится к
версии 0.1.3 из исходника.
Сборка из исходника и установка с диска
JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew :tools:mel-idea-plugin:buildPlugin
# → tools/mel-idea-plugin/build/distributions/mel-idea-plugin-0.1.3.zip
Нужен JDK 21. Сборка идёт против локально установленной Android Studio, дистрибутив
студии не скачивается. По умолчанию берётся ~/.local/share/JetBrains/Toolbox/apps/android-studio,
другой путь задаётся через -Pmel.androidStudioPath=… или ANDROID_STUDIO_PATH.
Какую студию выбрать для сборки, сказано в разделе «Совместимость».
Zip ставится через Settings → Plugins → ⚙ → Install Plugin from Disk…, после чего нужен
перезапуск. На Linux то же самое делает одна команда tools/mel-idea-plugin/update-plugin.sh:
она собирает zip и распаковывает его в каталог плагинов самой новой
~/.local/share/Google/AndroidStudio*. Другой каталог задаётся через
--plugins-dir=… или MEL_STUDIO_PLUGINS_DIR, а --restart
перезапускает студию.
Выкладка новой версии
Сначала поднимите version в tools/mel-idea-plugin/build.gradle.kts: студия
с уже установленным плагином видит обновление, только если версия выросла. Затем:
./gradlew melPublishPlugin -Pmel.outDir=mel-maven # собрать, положить в mel-maven/mel-plugins, закоммитить
./gradlew melPublishPlugin -Pmel.outDir=mel-maven -Pmel.push=true \
-Pmel.publish.remote=<remote> # то же и запушить
Задача melPublishPlugin собирает плагин, кладёт zip и updatePlugins.xml в
трекаемую папку mel-maven/mel-plugins и коммитит её. В папке держится ровно один zip:
старый удаляется, дескриптор ссылается на новый.
- версия — из имени zip;
id, имя иsince-build— из пропатченного при сборкеplugin.xml;- базовый raw-адрес для ссылки на zip в дескрипторе —
-Pmel.plugin.repoUrl=…; - «что нового» на экране обновления в студии —
-Pmel.plugin.notes="…".
Альтернатива — raw-репозиторий на Nexus через tools/mel-idea-plugin/publish-plugin.sh:
скрипт собирает zip, генерирует updatePlugins.xml и заливает оба HTTP PUT вместо коммита.
Версию он умеет поднять сам (--bump или --version X.Y.Z, после этого
закоммитьте build.gradle.kts), --dry-run только показывает дескриптор. Адрес
репозитория задаётся через MEL_PLUGIN_REPO_URL: значение по умолчанию в скрипте — заглушка.
Подробности обоих путей — в PUBLISHING.md репозитория sbbol_android_mel.
sbbol_android_mel боевых экранов нет: это библиотеки и инструменты. Плагин открывают на
проекте-потребителе. В sbbol (ветка develop) на MEL уже есть, например:
CreationPullerMachine(feature/creation) — часть переходов вынесена в функцию машиныopenDocument(…);LightBankChargesPullerMachine(feature/charges) — ветвление в цели:next(if (…) Empty else Content(…));BankSearchPullerMachine(feature/counterparties) иSelfRequisitesMachine(feature/payorder) — стейт-одиночка: состояние —data classбез sealed-иерархии;AccountRestrictionsMachine(feature/account) — sealed-граф сpush.
develop пока нет.
Совместимость
| Что | Значение |
|---|---|
| Минимальная сборка | sinceBuild = 253.32098.37 (Android Studio AI-253.32098.37, IntelliJ Platform 2025.3). untilBuild не задан. |
| Сборка | Против локально установленной студии (local(…) в build.gradle.kts). Собирать стоит на студии минимальной версии: байткод, собранный против более новой, может ссылаться на API, которых в 253 нет. JDK 21. Из точек Analysis API с линией 261 (Kotlin 2.3.0) явно сверен sealedClassInheritors, остальные защищены откатом на эвристику (см. ниже). |
| Нужные плагины IDE | Kotlin, Java, Android (org.jetbrains.android), Android Design Tools (com.android.tools.design). Все они бандлены в Android Studio, на неё плагин и рассчитан. |
| Режим Kotlin | K2: supportsK2="true" в plugin.xml. Без этой пометки студия в K2-режиме считает плагин несовместимым. |
Превью рисуется через внутренний нестабильный API рендера Android Studio, и с API 261 он поменялся:
RenderTask.dispose()удалён, вместо негоdisposeAsync();RenderResult.getRenderedImage()теперь возвращаетRecyclableImageвместоImagePool.Image.
Прямой вызов привязывает байткод к одной версии: собранный против одной студии плагин падал на
другой с NoSuchMethodError, а исходник с disposeAsync() не компилировался
против студии прежнего поколения. Поэтому оба вызова идут рефлексией: берётся тот метод, что есть в
текущей студии, а getCopy() есть у обоих типов картинки. Рендер превью работает и до, и
после смены API.
Вывод: sinceBuild без untilBuild от такой поломки не защищает.
Компиляцию против целевой студии надо гонять как регрессию, но такой проверки в репозитории пока
нет.
- Резолвер типов. Если три точки Analysis API на другой сборке не найдутся
(
LinkageError,NoSuchMethodError), резолвер молча откатится на эвристику по именам. Граф всё равно построится, но часть целей может стать «?». - Превью вне read-action. Перечисление
@Previewи рендер идут вне read-action в расчёте на поведение 2026.1. На других версиях это может падать. - Инспекции используют только стабильные API:
LocalInspectionToolи Kotlin PSI. Их тесты гоняются на IntelliJ IDEA Community 2025.1.
Как читать граф: минимальный словарь
MEL описывает экран явной стейт-машиной в духе Elm, поэтому экран можно нарисовать конечным графом. Ниже ровно столько терминов, сколько нужно, чтобы понимать картинку плагина. Сама архитектура объяснена в руководстве: словарь State/Intent/Effect — в шаге 1, секции — в шаге 8.
| Понятие | В коде | В плагине |
|---|---|---|
| Состояние | лист sealed-типа S из MelMachine<S, I, E>; если S — конкретный класс без sealed-иерархии, он сам единственное состояние | Узел графа. Цвет рамки показывает роль: зелёная — обычное, синяя — оверлей, янтарная — терминальное. |
| Факты | параметры конструктора состояния | Строки в теле узла (до шести, дальше «… +N»). Полный список — в инспекторе. |
| Интент | лист sealed-типа I | Подпись на ребре. |
| Клетка | on<S, I> { … }; onAny<I> — для любого состояния | Ребро «состояние —интент→ цель». Если тело клетки или цель next/push ветвится, на каждую ветку рисуется своё ребро. |
| Операция стека | next / push / pop / popTo / stay в теле клетки |
|
| Команда | command(…) / commandWithProgress(…) в переходе | Ребро зелёным пунктиром. Ключ и интенты-результаты показаны в инспекторе. |
| Подписка | subscribe(key, active<S>(), …) { … } | Строка со значком трансляции внутри узлов своего скоупа. |
| Эффект | MelEffect в переходе | На холсте не рисуется. Виден в инспекторе и на уровне клеток. |
| Старт | start() (startNext) | Бейдж START на стартовом состоянии. |
| Оверлей | overlay<S> в MelScreen | Синяя рамка и бейдж OVERLAY. |
| Терминальное | нет исходящих переходов; выводится из графа, а не объявляется | Янтарная рамка и бейдж TERMINAL. |
| Недостижимое | нет входящих переходов, и это не старт | Бейдж NO REACH. |
| Секция | класс Section<S, I, E>; клетки и подписки — в register() | Чип ▸N на узле показывает число секций. Клик по чипу раскрывает их. |
| Неразрешённая цель | цель next/push, которую не удалось вычислить статически | Ребро в янтарный узел «?». |
На узле показывается один бейдж в порядке приоритета: NO REACH → START → OVERLAY → TERMINAL.
Что плагин умеет сейчас
Всё ниже реализовано в исходнике :tools:mel-idea-plugin и :tools:mel-graph.
Разделы идут по видам тул-окна, затем инспекции и внутренняя механика.
MelScreenIndex по
словарному индексу находит файлы, в коде которых встречается MelMachine, подтверждает
наследника по PSI и собирает все .kt из поддерева каталога машины. Каждый пакет-экран
разбирает MelGraphExtractor из :tools:mel-graph. Это чистый PSI, и его
исходник компилируется прямо в плагин. Результат — ScreenGraph. Типы резолвит
AnalysisApiTypeResolver на K2 Analysis API, при сбое — эвристика по именам. Из графа
считаются производные (линтер, сводка, карта), и все виды рисуют одну и ту же модель.
Тул-окно и виды
| Возможность | Что делает |
|---|---|
| Каркас | Тул-окно MEL справа. Слева каталог (≈ 28 % ширины), справа колонка с собственным тулбаром и сменными видами, внизу строка статуса: «N экранов · M скрыто», во время скана — «Сканирую проект…». |
| Переключатель видов | Сегменты Граф состояний · Превью · Проблемы. Четвёртый вид, «Карта экранов», собран, но выключен флагом (см. ниже). Выбранный вид и открытый экран запоминаются между сессиями, отдельно для каждого проекта. |
| Тулбар |
|
| Единая точка выбора | Открытый экран, выбранное состояние, вид и запрос хранит MelSelectionController. Панели не вызывают друг друга, а шлют ему намерения и слушают его уведомления. Поэтому состояние, выбранное на графе, сразу показывают инспектор и превью. |
Каталог экранов
| Возможность | Что делает |
|---|---|
| Обнаружение | Словарный индекс отбирает только файлы, в коде которых есть слово MelMachine. Затем PSI проверяет супертип: прямой или транзитивный в пределах файла. Весь проект не парсится. |
| Группировка по пакетам | В группу экрана попадают все .kt из поддерева каталога машины. Каждый файл достаётся самому длинному пакету-машине-предку. Так контракт, оверлеи и секции из под-пакетов приходят к своему экрану без коллизий одноимённых состояний. |
| Список | В строке: имя экрана без суффикса Machine/Screen, укороченное имя Gradle-модуля, метка test. В тултипе: пакет и число состояний, переходов и подписок. Боевые экраны идут раньше тестовых, внутри — по полному имени. |
| Фильтры | Поиск по имени, модулю и пакету. Выпадающий список модулей. Кнопка test показывает тестовые экраны: по умолчанию они скрыты, их число видно в статусе. |
Граф состояний
| Возможность | Что делает |
|---|---|
| Раскладка | ELK 0.9.1: алгоритм layered (Sugiyama) сверху вниз, рёбра-сплайны, подписи размещает сам ELK. При любой ошибке ELK граф раскладывается встроенным алгоритмом (BFS-ранги от старта и барицентр) с прямыми рёбрами. |
| Узлы | Скруглённые карточки на Java2D. Внутри: имя, до шести фактов имя: тип, подписки скоупа. В шапке справа: бейдж роли, чип ▸N секций, чип ↻N действий на месте, кнопка <> «к коду». |
| Рёбра | Стиль зависит от операции и команды (см. словарь). Подпись — интент, плюс debounce в мс, если он задан. Самопетли и stay на холст не выносятся. |
| Псевдо-узлы |
|
| Зум по секциям |
|
| Подсветка |
|
| Легенда и тема | Сворачиваемая легенда в правом нижнем углу показывает цвета ролей и стили переходов. Все цвета заданы через JBColor, поэтому граф следует светлой и тёмной теме IDE. |
Инспектор
Панель справа от графа. Ширина задаётся сплиттером и запоминается. Это HTML-панель с упорядоченной палитрой, где каждая строка открывает исходник.
| Режим | Что показывает |
|---|---|
| Ничего не выбрано |
Сводка экрана:
|
| Выбрано состояние |
Четыре блока:
|
Снять выбор можно кликом по пустому месту графа или клавишей Esc.
Превью
| Возможность | Что делает |
|---|---|
| Живой кадр | Слева кадр состояния, отрендеренный Layoutlib самой студии, без Gradle и screenshot-тестов. Модель строится так же, как у встроенного Compose Preview, поэтому параметры @Preview учитываются: устройство, API, uiMode, fontScale, locale. Справа упрощённый граф: только состояния и по одной стрелке на пару. Клик по состоянию показывает его кадр. |
Подбор @Preview | Превью ищется в классах пакетов <пакет машины>.ui и самого пакета машины, в крайнем случае — в файле машины. Имя превью сравнивается с состоянием по убыванию приоритета: точное <State>Preview, суффикс …<State>Preview, просто содержит имя состояния. Нужен Android-модуль с фасетом Android. |
| Устойчивость |
|
| Нет кадра | Показывается одна общая заметка «@Preview не найден / рендер не дал кадр». Причины не различаются, а выбранный при неточном совпадении @Preview не показывается (это запланировано, см. «Что дальше»). |
Проблемы (граф-линтер)
Вид показывает результат lintScreen из :tools:mel-graph. Это чистая функция
над графом, покрытая тестами. Проблемы сгруппированы по категориям с числом находок. У каждой есть
заголовок, объяснение и ссылка в исходник. Если проблем нет, вид пишет «Проблем не видно».
| Категория | Когда срабатывает |
|---|---|
| Недостижимые состояния | Нет входящих переходов. |
| Тупики | Нет исходящих переходов, а состояние не терминальное. |
| Неразрешённые цели | Цель next/push/popTo не вычислилась статически. |
| Переходы в несуществующий стейт | Цель разрешилась в имя, которого нет среди листьев. |
| Старт не определён | Нет start() или его цель не разрешилась. |
| Результаты команд без клетки | Команда рождает интент, который не обрабатывает ни одна клетка. |
| Подписки без onError | Только на падучем источнике. Молчит для горячего потока, для .catch выше по цепочке, для flowOf/emptyFlow и для шины listen*/observe*/.asFlow(). |
| Подписки на горячем источнике | MutableStateFlow, stateIn, shareIn и подобные. Если onError задан, отмечается, что он мёртв. |
| Эмиты подписок без клетки | Подписка шлёт интент, для которого нет клетки. |
| Подписки на несуществующий стейт | Скоуп подписки ссылается не на лист. |
| Оверлеи без push | Состояние объявлено оверлеем, но его никто не кладёт в стек. |
| next в оверлей вместо push | next ведёт в состояние-оверлей: заменит экран вместо наложения, вероятно, нужен push. Находкой сейчас считается и обновление оверлеем собственных фактов через next(s.copy(…)). |
| Интент с несколькими клетками | Одна пара «состояние · интент» обработана в теле машины и в секции либо в двух секциях. Ветвление одной клетки находкой не считается. |
Карта экранов (выключена)
Кросс-экранная карта навигации: по узлу на экран и по связи на каждый эффект навигации (по префиксу
имени: NavigateTo…, Navigate…, GoTo…). Цель связи ищется по
совпадению метки с отображаемым именем экрана. Если совпадения нет, связь остаётся «выходом наружу».
Карта рисуется тем же холстом с паном и зумом. Клик по экрану открывает его граф, а инспектор
показывает список связей.
Сейчас вид выключен флагом mapEnabled = false в
MelToolWindowFactory.kt и в переключателе не появляется. Если в прошлой сессии была
выбрана карта, при открытии тул-окно переключится на граф. Код готов (MelScreenMapView,
MelScreenMapPanel), а включение ждёт проверки производительности на большом проекте.
Навигация в код и холст
| Возможность | Что делает |
|---|---|
| Переход к исходнику |
Куда ведёт клик:
OpenFileDescriptor по SourceRef (файл и смещение).
|
| Выбор | Клик по узлу выбирает состояние, и инспектор показывает его детали. Клик по пустому месту или Esc снимает выбор, а на уровнях секций поднимает на уровень вверх. Новый экран открывается без выбора, со сводкой в инспекторе. |
| Пан и зум | Граф двигается перетаскиванием, колесо масштабирует вокруг курсора в пределах 0,3×–3×. При открытии экран вписывается в окно, но не крупнее 1:1. |
Экспорт
| Возможность | Что делает |
|---|---|
| Экспорт PNG | Текущий вид графа в масштабе 1:1 сохраняется в файл через диалог сохранения. На уровне секций сохраняется то, что на экране. |
| Копировать Mermaid | MermaidEmitter собирает flowchart graph TD и кладёт его в буфер обмена:
|
| JSON (в ядре) | JsonEmitter из :tools:mel-graph выдаёт компактный JSON графа для программных потребителей. В тул-окне он не выведен. |
Инспекции в редакторе
В plugin.xml зарегистрировано 26 localInspection для Kotlin: группа
«MEL», уровень Warning, включены по умолчанию. На каждое правило detekt-набора
:tools:detekt-rules приходится одна инспекция с тем же именем и суффиксом
Inspection.
- Проверки работают только на PSI, без type resolution, поэтому подсвечивают нарушения на лету в K2-редакторе.
- Логика не дублируется: PSI-хелперы
MelPsi.ktлежат в source-root, общем с detekt-правилами, так что эвристика у них совпадает. - Тесты — отдельный модуль
:tools:mel-idea-inspections-test: 130 кейсов инспекций на лёгкой тест-платформе (IntelliJ IDEA Community 2025.1), без Android.
Quick-fix есть у двух инспекций. MelSubscribeOnError предлагает «Добавить
onError = { … }» и вставляет заглушку с TODO. MelDataContractLeaf предлагает
«Сделать data class», только если у класса есть параметры конструктора. Остальные 24 инспекции
только подсвечивают нарушение. Что проверяет каждое правило, описано на странице
mel-detekt.
| Правило / инспекция | Название в IDE |
|---|---|
MelSubscribeOnError · quick-fix | Подписка MEL без onError (§4.2) |
MelHotFlowSubscriptionSource | Горячий источник подписки MEL (onError мёртв) |
MelBlockingInCommandOrSubscription | Блокирующий вызов в команде/подписке MEL |
MelNoManualEffectCollection | Ручной сбор effects MEL |
MelNoRouterCloseEffect | Прямой router.close() в эффект-слое MEL |
MelNoCoroutinesInMachine | Корутины внутри машины/секции MEL |
MelStatelessMachine | Машина MEL должна быть stateless |
MelNoSendInReduce | send() внутри машины/секции MEL |
MelNoRawFrameworkConstructor | Прямой конструктор каркаса MEL (нужен DSL graph { }) |
MelCommandOnErrorMustReturnIntent | throw в onError команды/подписки MEL |
MelNoThrowInReduce | throw в reduce MEL |
MelNoMutationInReduce | Мутация захваченного поля в reduce MEL |
MelNoSideEffectsInReduce | Сайд-эффекты в reduce MEL |
MelNoNonDeterminismInReduce | Недетерминизм в reduce MEL |
MelNoBlockingInReduce | Блокирующий вызов в reduce MEL |
MelImmutableFacts | Иммутабельность стейта/фактов MEL (§4.5) |
MelNoInstrumentsInState | UI-инструменты в стейте/фактах MEL (§5) |
MelEffectsAreData | Эффект MEL должен быть данными (§5) |
MelNoMutableEffectContext | Иммутабельность эффект-контекста MEL (§5) |
MelNoTypeChecks | is/as по контракту MEL (§5) |
MelDataContractLeaf · quick-fix | Лист контракта MEL должен быть data |
MelNoLambdaInContract | Лямбда/функциональный тип в контракте MEL |
MelNoPlatformTypesInContract | Платформенные типы в контракте MEL |
MelNoBehaviorInContract | Поведение (функции) в контракте MEL |
MelSealedContractRoot | Корень контракта MEL должен быть sealed |
MelMigrationRatchet | Наследование старого MVI вне allowlist (§9) |
Живые обновления
| Возможность | Что делает |
|---|---|
| Правка кода | PSI-листенер следит только за физическими KtFile и после дебаунса 700 мс запускает лёгкий пере-скан. Кэш ведётся по пакетам: пересчитывается только пакет, у файлов которого сдвинулся штамп модификации. Правка чужого файла ничего не пересчитывает. |
| Gradle sync | После синхронизации идёт полный пере-скан со сбросом кэша, и превью перерисовывается на свежем classpath. |
| Скан вне EDT |
Скан идёт неблокирующим read-action:
|
| Что переживает пере-скан | Экран восстанавливается по полному имени, а если его больше нет — берётся первый. Если граф открытого экрана не изменился, виды не перестраиваются, и зум, уровень секций и кадр превью остаются на месте. Выбор состояния держится, пока такое состояние существует. |
Извлечение графа и резолвер типов
| Возможность | Что делает |
|---|---|
| Ядро на чистом PSI | MelGraphExtractor строит граф по уже разобранным KtFile и использует только org.jetbrains.kotlin.psi.*. В плагин компилируется его исходник против PSI студии. Тот же код без IDE гоняется в тестах :tools:mel-graph. |
| Что извлекается |
TransitionShapesTest.
|
| Секции | Классы Section<S, …> находятся структурно, по всем файлам пакета. Тело register() даёт клетки и подписки с пометкой секции; клетка секции может быть и ссылкой на метод (::confirmSign). Поэтому секции видны и при подключении через DI, когда их имён в коде машины нет. |
| Резолвер типов | Точка расширения — TypeResolver. В IDE её реализует AnalysisApiTypeResolver на K2 Analysis API: листья через sealedClassInheritors, цели через expressionType. При любом сбое, кроме отмены, он откатывается на NameHeuristicResolver: листья ищутся обходом иерархии в объединённом файле пакета, цель — по имени конструктора, ведущему идентификатору или типу параметра лямбды. |
| Best-effort | Неразрешённое не выбрасывается: цель становится null, на графе появляется «?», а в «Проблемах» — строка. Отдельные парсеры (эмиты подписок, результаты команд, длительности) работают по сужениям, описанным в KDoc экстрактора. |
Что дальше
Статусы:
- в планах — поведение продумано по сути, в коде его ещё нет;
- частично — часть уже есть, в карточке сказано, чего нет;
- идея — кандидат на обсуждение.
Сделанное описано в разделе «Что плагин умеет сейчас», например инспекции в редакторе и инспектор.
Вид «Структура»
в планахЭкран в виде дерева «состояние → секция → клетки и подписки» рядом с графом. Секции делят между собой клетки, а не состояния, поэтому на секционном экране почти вся логика — действия на месте вокруг одного состояния: диаграмма показывает такой экран одним узлом, а список — клетками, сгруппированными по секциям. Синхронизация с кареткой в обе стороны: клик по строке открывает код, каретка в редакторе подсвечивает свои строки. Штатная Structure View не подходит: она привязана к одному файлу, а MEL-экран живёт в нескольких.
Польза: секционный экран читается списком, а место в коде сразу видно в модели экрана.
Quick-fix к идиомам MEL
частичноЕсть у двух инспекций, для которых существует механически безопасный фикс:
MelSubscribeOnError (добавить пропущенный onError = {…}) и
MelDataContractLeaf (сделать data class, только при наличии параметров
конструктора).
Остальные 24 только подсвечивают. Кандидаты на фикс: throw в reduce →
эффект /ShowError, ручной collect потока → subscribe<>, сырой
конструктор фреймворка → MEL-фабрика, is-проверка → клетка on<>.
Польза: нарушение лечится одним нажатием, поэтому идиоматичный MEL писать и переносить с v3 дешевле.
Маркеры в gutter на клетках
в планахМаркер в полосе значков у on<S, I>, рождения машины и
subscribe. Маркер клетки называет пару «состояние × интент» и ведёт к целевому
состоянию. Неразрешённая цель получает отдельный маркер, который сообщает об этом, а не ведёт наугад.
Вне машин и секций маркеров нет.
Польза: прямо в редакторе видно, что лямбда — клетка и куда она ведёт.
Навигация по контракту (Find Usages)
в планахПереходы из контракта:
- от состояния — к обрабатывающим его клеткам и к регистрации view или оверлея;
- от интента — к клеткам и к местам отправки;
- от секции — к её клеткам и подпискам.
Результаты берутся из извлечённого графа, а не из текстового поиска. Если у состояния нет view, это сообщается явно.
Польза: интент трассируется сквозь контракт, машину и view одним жестом.
Секции на графе: зум и линза
частичноЗум есть: модель знает секцию каждой клетки и подписки, чип ▸N раскрывает секции состояния, а секция раскрывается в клетки (см. граф).
Линзы нет. Задумано оставлять яркими выбранные секции и приглушать остальные без изменения раскладки, со сбросом при смене экрана.
Польза: на экране-концентраторе видна логика одной секции, а форма графа не меняется.
Честное превью
в планахРазличать «@Preview не найден» и «рендер не удался», называть состояние и показывать,
какой @Preview выбран при неточном совпадении имени. Сейчас вместо этого одна общая
заметка (Превью).
Польза: превью перестаёт молча подменять результат запасной заметкой.
Карта экранов в переключателе
в планахКарта готова, но выключена флагом (см. выше). План:
- прогнать построение на большом проекте, проверить время и поведение при изменении дерева исходников;
- при необходимости сделать построение ленивым и отменяемым;
- снять флаг.
Польза: чтение на уровне проекта, а не одного экрана.
Вкладка миграции MVI v3 → MEL
в планахОтдельная вкладка тул-окна. Она находит конструкции v3 (Mutator и хендлеры, Listen-эффекты,
type-checks) и предлагает MEL-маппинг: хендлеры → on<>, Listen-эффект → команда
или подписка, переформовка sealed. Примеры — как в mel-migration.
В коде ничего нет.
Польза: ведёт по повторяющимся шагам переноса легаси-экранов, а это главный барьер внедрения.
Подсветка неразрешённых целей
частичноЕсть: ребро в янтарный узел «?» на графе и категория «Неразрешённые цели переходов» в «Проблемах» с переходом в код.
Нет: особого стиля «висячего» ребра и подсказки, как сделать цель статически анализируемой.
Польза: границы статического анализа видны и подталкивают код к формам, понятным графу и ревьюеру.
Линты покрытия / мёртвых интентов
частичноЕсть: «Проблемы» ловят недостижимые состояния, а также результаты команд и эмиты подписок, которые не обрабатывает ни одна клетка. На графе из этого видно только бейдж NO REACH.
Нет: поиска интентов контракта, которые не обрабатывает ни одна on<>.
Модель графа не хранит список интентов контракта.
Польза: ловит неполные и устаревшие машины, которые компилируются, но содержат мёртвое или отсутствующее поведение.
Находки поверх графа
идеяБейджи на узлах и рёбрах по находкам detekt и граф-линтера в их исходном диапазоне, с кликом в фикс. Сейчас находки линтера живут только в виде «Проблемы».
Польза: архитектурный линт связан с визуальной моделью, проблема видна там, где живёт нарушающее поведение.
Экспорт графа в docs-сайт
частичноMermaid и PNG плагин уже отдаёт. Нет действия, которое кладёт диаграмму экрана сразу в формат
сайта mel-docs, без ручного копирования.
Польза: опубликованная документация остаётся в синхроне с кодом без ручного сопровождения диаграмм.
Слой эффектов / команд / подписок
идеяВторой режим диаграммы: команды и подписки становятся узлами и связываются с состояниями, которые их взводят, и с интентами, которые они подают обратно. Ранний вид «Эффекты» был заменён «Проблемами». Сейчас команды, эффекты и подписки видны в инспекторе и на уровне клеток.
Польза: невидимый async-слой и циклы обратной связи становятся явными.
Minimap и focus-режим
идеяMinimap для больших машин и режим, гасящий всё, кроме выбранного узла и его соседей на расстоянии одного ребра. Сейчас выбор узла приглушает только несмежные рёбра, а поиск — несовпавшие узлы.
Польза: большие экраны остаются навигируемыми.
Интерактивный симулятор / степпер
идеяЗадать стартовое состояние и кликать интенты: граф подсвечивает активное состояние, показывает факты результата, эффекты и интенты-результаты команд. Это сухой прогон по модели, без исполнения.
Польза: рассуждать о достижимости без сборки и тапов по живому экрану.
Git-дифф графа экрана
идеяСравнить граф между двумя ревизиями и показать с цветовой разметкой добавленные и удалённые состояния и рёбра, ключи команд, debounce, эффекты, скоупы подписок.
Польза: изменения поведения видны в терминах ревьюера, а не стеной Kotlin-диффа.
Мастер нового экрана
идеяДействие New → MEL Screen генерирует контракт, скелет машины, Compose-view и проводку
melViewModel. Это фаза письма: инструмент для написания экранов, а не для чтения.
Польза: новые экраны идиоматичны с первого коммита.
Live-templates / postfix MEL
идеяШаблоны для частых клеток: on<S,I>{ … }, start(),
subscribe<>(key){} с onError, блок команды с результатами, лист-состояние с
фактами. Это тоже фаза письма.
Польза: ускоряет ручное письмо машин и навязывает правильную форму, например всегда предлагает слот onError.
Как устроено
Выбранные технологии по слоям. Плагин не объявляет graph-библиотек, кроме ELK, и не использует JCEF.
| Слой | Что выбрано | Где в коде |
|---|---|---|
| Извлечение | PSI самой студии и исходник :tools:mel-graph, подключённый как source-root: экстрактор, линтер, производные, карта, эмиттеры. | tools/mel-graph/src/main/kotlin, MelScreenIndex.kt |
| Резолв типов | K2 Analysis API с откатом на эвристику по именам. | AnalysisApiTypeResolver.kt |
| Раскладка | ELK 0.9.1 (layered и сплайны) как геометрический движок. Фолбэк — свой послойный алгоритм. | MelElkLayout.kt, MelGraphLayout.kt |
| Холст | Свой Java2D: база MelCanvas отвечает за пан, зум и вписывание. Цвета — JBColor из единой палитры. | MelCanvas.kt, MelGraphView.kt, MelPalette.kt |
| Панели-отчёты | HTML в JEditorPane со ссылками в исходник: инспектор, «Проблемы». | HtmlReportPane.kt и наследники |
| Обвязка | ToolWindowFactory, JBSplitter, JBList, SearchTextField, ComboBox, ActionToolbar, свой сегментный переключатель. | MelToolWindowFactory.kt, CatalogPanel.kt, MelSegmentedControl.kt |
| Состояние UI | Единственная точка истины: панели шлют намерения и слушают уведомления на EDT. | MelSelectionController.kt |
| Превью | Внутренний API рендера Android Studio: Layoutlib, модель как у встроенного Compose Preview. Методы, сменившие сигнатуру между версиями студии, вызываются рефлексией. | MelComposeRenderer.kt, PreviewPanel.kt |
| Инспекции | LocalInspectionTool и общий PSI source-root с detekt-правилами. | inspection/, tools/detekt-rules/src/psi/kotlin |
- Нет JCEF. В JBR целевой студии нет нативного CEF, поэтому браузерный рендер (Mermaid.js, D3) отпал, и граф рисуется своим Java2D.
- Исходник вместо jar. Jar
mel-graphсобран противkotlin-compiler-embeddable, которого в IDE нет. Поэтому в плагин компилируется исходник ядра; тем же приёмом подключены PSI-хелперы detekt-правил. - ELK без Guava. ELK забандлен в плагин, но Guava из его зависимостей исключена и берётся из
платформы. Вторая копия давала
LinkageErrorпри рендере превью.
Тесты:
:tools:mel-graph:test— экстрактор на эвристическом резолвере (включая формы переходов боевых экранов,TransitionShapesTest), линтер, производные, карта экранов, эмиттеры, классификация источников подписок;:tools:mel-idea-inspections-test— инспекции на лёгкой платформе.
IDE-резолвера на Analysis API и самого тул-окна тесты не касаются.
Источник истины по поведению — код в :tools:mel-idea-plugin и :tools:mel-graph
репозитория sbbol_android_mel, по архитектуре MEL — :core:mel, затем
спека. Если документ разойдётся с кодом, прав код.