Claude Code для итерации UI в Flutter: практический цикл, превращающий пользовательские истории в дерево виджетов, состояние и навигацию, сохраняя изменения модульными и удобными для ревью.

Быстрая работа с Flutter часто начинается хорошо. Вы подправляете верстку, добавляете кнопку, сдвигаете поле — и экран быстро становится лучше. Проблемы проявляются через несколько итераций, когда скорость превращается в гору изменений, которые никто не хочет ревьюить.
Команды обычно сталкиваются с одинаковыми провалами:
Большая причина — подход «один большой промпт»: описать фичу целиком, попросить все экраны и принять большой вывод. Ассистент пытается помочь, но трогает слишком много мест в коде одновременно. Изменения становятся грязными, их сложно ревьюить и рискованно мёржить.
Повторяемая петля решает это, заставляя думать и ограничивая радиус поражения. Вместо «построить фичу» делайте это циклично: выберите одну пользовательскую историю, сгенерируйте минимальный UI‑срез, который её подтверждает, добавьте только то состояние, что нужно этому срезу, затем пробросьте навигацию для одного пути. Каждый проход остаётся достаточно маленьким, чтобы его можно было ревьюить, а ошибки легко откатить.
Цель — практичный рабочий процесс, превращающий истории в конкретные экраны, обработку состояния и навигационные потоки без потери контроля. При хорошем подходе вы получите модульные куски UI, меньшие диффы и меньше сюрпризов при изменении требований.
Пользовательские истории написаны для людей, а не для дерева виджетов. Прежде чем генерировать что‑то, преобразуйте историю в небольшой UI‑спек, описывающий видимое поведение. «Готово» должно быть тестируемым: что пользователь видит, на что нажимает и что подтверждает — а не то, что дизайн «кажется современным».
Простой способ сохранить объём конкретным — разбить историю на четыре блока:
Если история всё ещё мутная, ответьте на эти вопросы простым языком:
Добавляйте ограничения заранее — они направляют каждое решение по верстке: базовые темы (цвета, отступы, типографика), адаптивность (сначала телефон в портрете, затем планшеты) и минимумы доступности — размер таргета для тапов, масштабируемость текста, понятные метки для иконок.
Наконец, решите, что стабильно, а что гибко, чтобы не трясти кодовую базу. Стабильные вещи — те, от которых зависят другие фичи: имена маршрутов, модели данных, существующие API. Гибкие — безопаснее для итераций: структура верстки, микрокопирайт, точный состав виджетов.
Пример: «Как пользователь, я могу сохранить товар в Избранное со страницы деталей.» Понятный UI‑спек может быть:
Этого достаточно, чтобы построить, ревьюить и итеративно править без домыслов.
Маленькие диффы не означают медленную работу. Они делают каждое изменение лёгким для ревью, простым для отката и трудным для разрушения. Самое простое правило: одна страница или одно взаимодействие за итерацию.
Выберите узкий срез перед началом. «Добавить пустое состояние в Orders screen» — хороший срез. «Переделать весь Orders flow» — нет. Цель — дифф, который коллега поймёт за минуту.
Стабильная структура папок тоже помогает держать изменения локализованными. Простая «feature‑first» раскладка предотвращает разброс виджетов и маршрутов по проекту:
lib/
features/
orders/
screens/
widgets/
state/
routes.dart
Держите виджеты маленькими и составными. Когда виджет имеет ясные входы и выходы, вы можете менять верстку без правки логики состояния, и менять состояние без переписывания UI. Предпочитайте виджеты, которые принимают простые значения и callbacks, а не тянут глобальное состояние.
Цикл, который остаётся ревью‑дружественным:
Установите жёсткое правило: каждое изменение должно быть легко откатимо или изолируемо. Избегайте «drive‑by» рефакторов во время итерации экрана. Если замечаете посторонние проблемы — запишите их и исправьте в отдельном коммите.
Если инструмент поддерживает snapshotы и откат, используйте каждый срез как точку снимка. Некоторые платформы включают snapshot/rollback, что безопаснее при смелых экспериментах с UI.
Ещё одна привычка, которая смягчает ранние итерации: предпочитайте добавлять новые виджеты, а не править общие. Общие компоненты — именно место, где маленькие изменения превращаются в большие диффы.
Быстрая работа с UI безопасна, когда вы отделяете размышление от набора кода. Начните с чёткого плана дерева виджетов перед генерацией кода.
Попросите только outline дерева виджетов. Хотите имена виджетов, иерархию и что каждая часть отображает. Никакого кода. Здесь вы находите пропущенные состояния, пустые экраны и странные решения по верстке, пока всё ещё дешево менять.
Попросите разбиение компонентов с их ответственностями. Держите каждый виджет сфокусированным: один рендерит заголовок, другой — список, третий — пустое/ошибочное состояние. Если что‑то потребует состояния позже, отметьте это сейчас, но не реализуйте.
Сгенерируйте scaffold экрана и stateless виджеты. Начните с одного файла экрана с плейсхолдерами и понятными TODO. Держите входы явными (параметры конструктора), чтобы позже подставлять реальное состояние без переписывания дерева.
Сделайте отдельный проход для стилизации и деталей верстки: отступы, типографика, тема, адаптивность. Трактуйте стили как отдельный дифф, чтобы ревью было проще.
Ставьте ограничения в начале, чтобы ассистент не выдумал несоответствующий UI:
Конкретный пример: история — «Как пользователь, я могу просматривать сохранённые товары и удалять один.» Попросите дерево виджетов с app bar, списком строк и пустым состоянием. Затем запросите разбиение: SavedItemsScreen, SavedItemTile, EmptySavedItems. Только после этого генерируйте scaffold со stateless виджетами и фейковыми данными, а стили добавляйте отдельно (divider, padding, явная кнопка удаления).
Итерация UI рушится, когда каждый виджет начинает принимать решения. Держите дерево «глупым»: оно должно читать состояние и рендерить, а не содержать бизнес‑правила.
Начните с именования состояний словами. Большинству фич нужно больше, чем просто «loading» и «done»:
Затем перечислите события, которые могут менять состояние: тапы, отправка формы, pull‑to‑refresh, назад, retry, «пользователь изменил поле». Проработка этого заранее предотвращает догадки позже.
Выберите один подход к состоянию для фичи и придерживайтесь его. Цель не «лучший паттерн», а согласованные диффы.
Для небольшого экрана простого контроллера (ChangeNotifier или ValueNotifier) часто достаточно. Поместите логику в одно место:
Прежде чем писать код, опишите переходы состояний простыми предложениями. Пример для экрана логина:
"Когда пользователь нажимает Sign in: поставить Loading. Если email невалиден: остаться в Partial input и показать строчное сообщение. Если пароль неверен: установить Error с сообщением и включить Retry. При успехе: установить Success и навигировать на Home."
Дальше сгенерируйте минимальный Dart‑код, соответствующий этим правилам. Ревью остаётся простым, потому что дифф можно сравнить с набором правил.
Сделайте валидацию явной. Решите, что делать при неверных вводах:
Когда эти ответы задокументированы, UI остаётся чистым, а код состояния — компактным.
Хорошая навигация начинается с маленькой карты, а не из кучи роутов. Для каждой пользовательской истории опишите четыре момента: где пользователь входит, какой следующий наиболее вероятный шаг, как отменить, и что означает «назад» (вернуться к предыдущему экрану или к безопасному домашнему состоянию).
Простая карта маршрутов должна отвечать на вопросы, которые обычно вызывают переделки:
Затем опишите параметры, передаваемые между экранами: идентификаторы (productId, orderId), фильтры (диапазон дат, статус) и черновые данные (частично заполненная форма). Если этого не зафиксировать, в итоге вы спрячете состояние в глобальные синглтоны или будете перестраивать экраны, чтобы «найти» контекст.
Deep links важны даже если вы не выпускаете их в день 1. Решите, что происходит, когда пользователь попадает в середину потока: можно ли подгрузить недостающие данные или нужно перенаправить на безопасный входной экран?
Также определите, какие экраны возвращают результат. Пример: экран «Выбрать адрес» возвращает addressId, и экран оформления заказа обновляется без полного рефреша. Держите форму возвращаемых данных маленькой и типизированной, чтобы изменения было легко ревьюить.
Перед кодом обозначьте пограничные случаи: несохранённые изменения (показать подтверждение), требующая авторизация (приостановить и возобновить после логина), удалённые данные (показать ошибку и понятный выход).
Когда вы итеративно правите быстро, реальный риск — не «неправильный UI», а неревьюируемый UI. Если коллеге непонятно, что изменилось, почему и что осталось стабильным, каждая следующая итерация замедляется.
Правило, которое помогает: сначала зафиксируйте интерфейсы, затем позволяйте двигать внутренности. Стабилизируйте публичные пропсы виджетов (входы), маленькие UI‑модели и аргументы маршрутов. Как только они именованы и типизированы, вы можете перестраивать внутреннее дерево без ломки остальной части приложения.
Попросите план, удобный для диффа, перед генерацией кода. Вы хотите план, который указывает, какие файлы изменятся, а какие должны остаться нетронутыми. Это фокусирует ревью и предотвращает случайные рефакторы, меняющие поведение.
Паттерны, которые держат диффы маленькими:
Пусть история: «Как покупатель, я могу изменить адрес доставки из checkout.» Сначала зафиксируйте аргументы маршрута: CheckoutArgs(cartId, shippingAddressId) должен оставаться стабильным. Затем итеративно правьте внутри экрана. Когда верстка устаканится, разбейте её на AddressForm, AddressSummary и SaveBar.
Если обработка состояния меняется (например, валидация уходит из виджета в CheckoutController), ревью остаётся читабельным: UI‑файлы в основном меняют рендеринг, а контроллер показывает логику в одном месте.
Самый быстрый путь замедлиться — просить ассистента изменить всё сразу. Если один коммит трогает верстку, состояние и навигацию, ревьюеры не поймут, что сломалось, и откат станет сложным.
Более безопасная привычка — одно намерение за итерацию: сначала дерево, затем состояние, затем навигация.
Одна распространённая проблема — позволять генерируемому коду придумывать новый паттерн для каждой страницы. Если одна страница использует Provider, вторая — setState, а третья вводит кастомный контроллер, проект быстро становится непоследовательным. Выберите небольшой набор паттернов и поддерживайте их.
Ещё одна ошибка — запуск асинхронной работы прямо в build(). В демо это может выглядеть ок, но это вызывает повторные вызовы при ререндере, мерцание и трудноуловимые баги. Перенесите вызов в initState(), view‑model или контроллер и храните build() для рендера.
Именование — тихая ловушка. Код, который компилируется, но выглядит как Widget1, data2 или temp, делает будущие рефакторы болезненными. Понятные имена помогают ассистенту генерировать лучшее последующее изменение, потому что намерение ясно.
Ограждения, предотвращающие худшие исходы:
build()Классический фикс визуальной проблемы — добавить ещё Container, Padding, Align и SizedBox, пока всё не выровняется. Через несколько проходов дерево становится нечитаемым.
Если кнопка не выровнена, сначала попробуйте убрать обёртки, использовать один родительский layout‑виджет или извлечь небольшой виджет со своими ограничениями.
Пример: на странице оформления заказа итоговая сумма прыгает при загрузке. Ассистент мог бы обернуть строку цены дополнительными виджетами, чтобы «стабилизировать» её. Более чистый фикс — зарезервировать пространство простым плейсхолдером загрузки, сохранив структуру строки.
Перед коммитом сделайте двухминутный проход, который проверит ценность для пользователя и убережёт от сюрпризов. Цель не совершенство, а убедиться, что итерация легко ревьюится, тестируется и откатывается.
Прочитайте историю и сверяйте эти пункты с запущенным приложением (или хотя бы с простым widget‑тестом):
Быстрая проверка в реальности: если вы добавили новый экран Order details, вы должны суметь (1) открыть его из списка, (2) увидеть спиннер загрузки, (3) симулировать ошибку, (4) увидеть пустой заказ, и (5) нажать назад и вернуться в список без странных скачков.
Если ваш процесс поддерживает snapshot/rollback, делайте снимок перед большими изменениями верстки — это поможет итеративно двигаться быстрее, не рискуя веткой main.
История: «Как покупатель, я могу просматривать товары, открывать страницу детали, сохранять товар в избранное и потом просматривать избранное.» Цель — перейти от текста к экранам в трёх маленьких, ревью‑дружественных шагах.
Итерация 1: сосредоточьтесь на экране списка. Создайте дерево виджетов, достаточно полное для рендера, но не привязанное к реальным данным: Scaffold с AppBar, ListView с плейсхолдерными рядами и явные UI для loading и empty. Держите состояние простым: loading (показывать CircularProgressIndicator), empty (сообщение и кнопка Try again), ready (показывать список).
Итерация 2: добавьте экран деталей и навигацию. Явно: onTap пушит маршрут и передаёт небольшой объект параметров (например: id товара, title). Начните details как read‑only с заголовком, плейсхолдером описания и кнопкой Favorite. Цель — соответствовать истории: list -> details -> back, без лишних потоков.
Итерация 3: введите состояние избранного и UI‑фидбек. Добавьте единую точку правды для избранного (пусть пока в памяти) и подключите её к обоим экранам. Тап по Favorite сразу обновляет иконку и показывает короткий SnackBar. Затем добавьте экран Favorites, который читает то же состояние и обрабатывает пустое состояние.
Типичный ревью‑дружественный дифф выглядит так:
browse_list_screen.dart: дерево виджетов + loading/empty/ready UIitem_details_screen.dart: разметка и приём navigation paramsfavorites_store.dart: минимальный хранилище состояния и методы обновленияapp_routes.dart: маршруты и типизированные хелперы навигацииfavorites_screen.dart: читает состояние и показывает empty/list UIЕсли какой‑то файл стал «местом, где происходит всё», вынесите части до продолжения. Маленькие файлы с понятными именами ускоряют следующую итерацию и делают её безопасной.
Если рабочий процесс действует только когда вы «в зоне», он развалится, как только вы переключитесь на другую задачу или кто‑то ещё тронет фичу. Сделайте цикл привычкой: опишите его и поставьте ограждения по размерам изменений.
Одна командная форма начала итерации даёт одинаковые входы и похожий вывод. Держите шаблон коротким, но специфичным:
Это уменьшает шанс, что ассистент придумает новый паттерн посреди фичи.
Выберите определение «маленького», которое легко применять в код‑ревью. Например, ограничьте итерацию количеством файлов и разделите рефакторы UI и поведенческие изменения.
Простые правила:
Добавьте чекпоинты, чтобы можно было быстро откатить плохой шаг. Минимально — тэги коммитов или локальные контрольные точки перед крупными рефакторами. Если процесс поддерживает snapshot/rollback, используйте их активно.
Если хотите чат‑ориентированный процесс, который генерирует и уточняет Flutter‑приложения end‑to‑end, некоторые платформы предлагают режим планирования, где вы сначала ревьюите план и ожидаемые файлы до применения изменений.
Используйте короткий тестируемый UI‑спек в первую очередь. Напишите 3–6 строк, которые покрывают:
Затем реализуйте только этот срез (обычно один экран + 1–2 виджета).
Преобразуйте историю в четыре блока:
Если вы не можете быстро описать чек‑лист приёмки — история ещё слишком расплывчата для чистого UI‑диффа.
Начните с генерации только outline‑дерева виджетов (имена + иерархия + что показывает каждая часть). Никакого кода.
Затем попросите разбиение по ответственностям компонентов (кто за что отвечает).
Только после этого генерируйте stateless scaffold с явными входными параметрами (значения + callbacks), а стили оформляйте отдельным коммитом.
Сделайте правило: один намерение на итерацию.
Если один коммит меняет верстку, состояние и маршруты одновременно, ревью не поймёт, что сломалось, и откат станет сложнее.
Держите виджеты «глупыми»: они должны рендерить состояние, а не принимать бизнес‑решения.
Практический дефолт:
Избегайте асинхронных вызовов в build() — это приводит к повторным вызовам при ререндере.
Определите состояния и переходы на бумаге перед кодом.
Пример шаблона:
Далее перечислите события, которые переводят между ними: refresh, retry, submit, edit. Так код легче сравнивать с прописанными правилами.
Нарисуйте небольшой «map» потока для истории:
И зафиксируйте параметры, которые передаются между экранами (IDs, фильтры, черновые данные). Это предотвратит прятание контекста в глобальных синглтонах.
По умолчанию — структура по фичам, чтобы изменения оставались в рамках:
lib/features/<feature>/screens/lib/features/<feature>/widgets/lib/features/<feature>/state/lib/features/<feature>/routes.dartДержите каждую итерацию сфокусированной на одной папке фичи и избегайте «drive‑by» рефакторов в других местах.
Правило простое: зафиксируйте интерфейсы, а не внутренности.
Ревьюерам важнее, чтобы входы/выходы оставались стабильными, даже если дерево поменялось внутри.
Быстрая двухминутная проверка:
Если процесс поддерживает snapshot/rollback — сделайте снимок перед большой перестройкой верстки.