Ревью · 24.09.2026 · код проекта не менялся

Ревью переноса на iOS

Детальный разбор перед началом работ по переносу «По душе» на Compose Multiplatform: что забэкаплено и как восстановить, что показали три пробы на макмини, какие версии совместимы, как устроить проект, что и как переносится по каждому файлу, порядок работ с контрольными точками. Продолжение разбора «По душе на iOS».

Дата: 24.09.2026. Статус: разбор выполнен, бэкап сделан, код проекта не менялся. Продолжение docs/41-ios-podushe-research-2026-09-24.md (общая картина и вопросы владельцу). Слово владельца 24.09: «можно попробовать, но сделать бэкап кода текущей версии; перед началом разработки провести более детальный ревью с изучением всех деталей более глубоко». Этот документ и есть тот ревью: каждое утверждение ниже проверено чтением кода, скриптом по всем 241 файлу или сборкой на макмини. Что проверить не удалось — сказано прямо (раздел 3.4).

0Коротко

  1. Бэкап сделан и проверен восстановлением (раздел 1): метки backup/pre-ios-2026-09-24-main и …-release в git и на GitHub, полный бандл всех 98 ссылок репозитория на сервере и на макмини (контрольные суммы сошлись), список всех веток отдельным файлом.
  2. Вывод после разбора: идти можно, блокеров нет. Из 241 файла Kotlin как есть переезжают 39, механически правятся 105 (25,9 тыс. строк: ресурсы, даты, строки), у 11 меняется библиотека, у 35 нужен второй вариант для iOS (14,3 тыс. строк, из них 4,5 тыс. — четыре больших экрана, где платформенного по нескольку строк). 33 файла кадров съёмки и 18 тестов — отдельные классы (раздел 2).
  3. Два прототипа на макмини собрались и запустились в симуляторе iPhone 17e: в первом — движок теней и наложений на Skia, стекло, навигация; во втором — SF Pro из байтов, векторные иконки из .fig, строки с подстановками и числительными, даты по-русски, Ktor до стенда dusha (ответ 200), DataStore, Coil, QR, JPEG из Skia, инсеты, ViewModel, типизированные маршруты, схема URL. Все найденные ловушки записаны (раздел 3).
  4. Главная находка ревью — версии. Новейший Compose 1.12 на Android требует AGP 9.1, а AGP 9 запрещает держать Kotlin Multiplatform и Android-приложение в одном Gradle-модуле; свежие Coil 3.6 и OkHttp 5.5 требуют compileSdk 37, которого в нашем SDK пока нет. Поэтому есть две дороги (раздел 4): Б — «Android как есть»: один модуль app, наши AGP 8.7.3, compileSdk 35, JDK 17, Paparazzi 1.3.5 и плагин Sentry не трогаются, Compose Multiplatform 1.11.1 (май 2026), Ktor 3.2, Coil 3.4 — совместимость подтверждена проверкой метаданных и полной сборкой (проба 3). Единственная поправка к «как есть»: Paparazzi 1.3.5 не рисует Compose 1.11 (падение в layoutlib), а Paparazzi 2.0 требует AGP 8.13 и JDK 21 — значит на дороге Б всё же AGP 8.7.3 → 8.13.2 (та же 8-я линия, один модуль разрешён) и JDK 21 на макмини; с этим набором APK, снимок Paparazzi и iOS-фреймворк собрались; А — «современный стек»: AGP 9.4 + Gradle 9.7 + compileSdk 37 + JDK 21, код в отдельном модуле shared, Compose 1.12.1 — обязательная инфраструктурная работа на будущее, но её лучше делать отдельно от iOS. Рекомендация — Б сейчас, А отдельной задачей после выхода iOS.
  5. Карта и Sentry на iOS — через Swift-мост, а не через кроссплатформенные библиотеки: MapLibre iOS и Sentry Cocoa подключаются в Xcode-проект как обычные Swift-пакеты, а Kotlin зовёт их через свои интерфейсы. Это снимает главный технический риск (нестабильный FFI у maplibre-compose, версия 0.x у Sentry KMP) и сохраняет Android-код нетронутым (раздел 5.4).
  6. Порядок работ уточнён по шагам с контрольными точками (раздел 8): Э0 без переноса файлов, Э1 обновление двигателя (на дороге Б — только Kotlin 2.4.20 и Compose 1.11, без смены AGP/Gradle/SDK) с пересъёмкой всех кадров, Э2 переезд одним коммитом в окно тишины, Э3 iOS-узлы, Э4 сверка, Э5 TestFlight. Оценка прежняя: 8–12 недель до сверенной сборки в TestFlight.

1Бэкап текущей версии кода (сделано 24.09.2026)

Что считается «текущей версией»: локальная main (2894cd60, снимок на начало сессии) и release/2026-09-23 (7807dc8e — из неё собрана сборка тестировщиков 0.2.0-260922.1904-dusha), плюс все остальные локальные ветки, включая те, которых нет на GitHub (4 ветки), и рабочие деревья соседних сессий.

ЧтоГдеПроверка
Метки git backup/pre-ios-2026-09-24-main → 2894cd60, backup/pre-ios-2026-09-24-release → 7807dc8eлокальный репозиторий и GitHub (git ls-remote --tags origin)видны на origin
Полный бандл всех ссылок plibvi-pre-ios-2026-09-24-full.bundle (458 МБ, 98 ссылок, «complete history»)/root/backups/ на сервере и ~/backups/ на макминиgit bundle verify на сервере; sha256 71ca59e2…1f31 совпала на маке
Прирост к бандлам 18.09 и 23.09 plibvi-pre-ios-2026-09-24-inc.bundle (43 КБ)там жеsha256 3ea81506…8de3; восстановление цепочкой 18.09 → 23.09 → 24.09 на маке: release/2026-09-23 и метки восстановились, main из прироста не выходит (её вершина — предок release, бандл такие ссылки не пишет) — поэтому основной бэкап полный бандл, прирост запасной
Список всех веток и меток с SHA plibvi-refs-2026-09-24.txt (76 строк)там же—
Рендеры Figma (623 PNG) и прочие материалы сверкине в git; копии на маке ~/podushe-render (623) и ~/podushe-work (5 785 PNG)посчитано 24.09

Восстановление (на любой машине с git):

git clone /root/backups/plibvi-pre-ios-2026-09-24-full.bundle plibvi   # или ~/backups/… на маке
cd plibvi && git checkout release/2026-09-23                              # или main

Правило на время работ: перед каждым этапом Э1–Э3 (раздел 8) — свежая метка backup/<этап>-<дата> и прирост бандла на мак; Android-сборки тестировщиков до конца Э4 идут из release/2026-09-23 (или её преемницы), не из ветки переноса.

2Метод разбора и его итог

Скрипт инвентаризации (tools/-кандидат, пока в черновике сессии) прошёл по всем .kt файлам app/src (241 файл, 55 396 строк), убрав комментарии (первая версия без этого давала ложные срабатывания: слово «Activity» в KDoc, BackHandler как «Handler»). Каждый файл получил класс по худшему признаку:

КлассСмыслФайловСтрок
Опереезжает в commonMain без правок394 609
Ммеханическая правка: R.* → Res.*, java.time → kotlinx-datetime, Locale, String.format, Log, BuildConfig, LocalContext10525 922
Бзамена библиотеки (Retrofit/OkHttp, Coil 2, Sentry Android, ZXing, DataStore с Context)111 364
Пнужен второй вариант (expect/actual) — есть вызовы android.*, нативный канвас, карта, камера, файлы, разрешения, окно3514 309
Дкадры съёмки (src/debug, DesignShots)337 074
Ттесты — остаются на JVM как Android unit tests182 118

Полные таблицы по файлам — приложение А. Что важно из них:

3Что проверено на макмини (две пробы 24.09.2026)

Обе пробы — отдельные проекты (~/cmp-smoke, ~/cmp-proto), не в ~/podushe-src; Kotlin 2.4.20, Compose Multiplatform 1.12.1, симулятор iPhone 17e (iOS 26.5, 1170×2532). Полные тексты проб — в черновике сессии; ниже факты.

3.1 Проба 1: движок вёрстки

Собралось и нарисовалось: Modifier.blur; GraphicsLayer + BlurEffect с тройной отрисовкой (модель PdGlassBackdrop); BlendMode.Plus в Compose; на Skia-канвасе — битмап ALPHA_8, MaskFilter.makeBlur, режимы PLUS и DST_OUT (модель FigDraw); PathParser из pathData ноды; BasicTextField с кириллицей; LazyColumn; два экрана NavHost. Фреймворк собирается за 1,5 мин, тулчейн Kotlin/Native (1,6 ГБ) скачался сам.

3.2 Проба 2: библиотеки и ресурсы

ЧтоКак провереноИтог
SF Pro из байтов вне композицииRes.readBytes("files/…ttf") → androidx.compose.ui.text.platform.Font(id, bytes, weight) в iosMain; Regular 400 и Condensed 650✅ узкое 650 — заголовок, обычное 400 — текст, буквы «ё», «Ъ», «₽» есть
Шрифт через ресурс в композицииFont(Res.font.sf_pro_semibold, FontWeight(590))✅
Векторные XML из .figpainterResource(Res.drawable.ic_bell_fill) + tint, ещё две иконки✅ рисуются с подкраской
Строки с подстановкамиstringResource(Res.string.fmt_ago_minutes, 5) → «5 мин назад»; %1$s → «2.4 км от вас»✅
ЧислительныеpluralStringResource(…, 3, 3) → «3 общих интереса»; вне композиции getPluralString(…, 21, 21) → «21 общий интерес»; getString✅ русские формы one/few/many работают
Датыkotlinx-datetime 0.8: LocalDate.Format { day(Padding.NONE); char(' '); monthName(RU) } → «24 сентября»; kotlin.time.Instant.parse("2026-09-24T10:00:00+03:00").toLocalDateTime(tz)✅ Europe/Moscow на маке
СетьKtor 3.6 Darwin: GET https://dusha.uxbrain.ru/api/v1/public/events?limit=1✅ 200, JSON
ХранилищеDataStore Preferences 1.2.1 через PreferenceDataStoreFactory.createWithPath в NSDocumentDirectory✅ пишет и читает
Картинки по сетиCoil 3.6.3 + coil-network-ktor3, setSingletonImageLoaderFactory с KtorNetworkFetcherFactory✅ логотип загрузился
QRqrose 1.3.0 rememberQrCodePainter✅
JPEG из растраorg.jetbrains.skia.Image.makeFromBitmap(bmp).encodeToData(JPEG, 90)✅ 1 202 байта
ИнсетыWindowInsets.safeDrawing: верх 47, низ 34 на iPhone 17e; WindowInsets.ime 0 без клавиатуры✅
ViewModel, lifecycleviewModel { CounterVm() }, LocalLifecycleOwner → RESUMED✅
Навигацияcomposable<Home>, composable<Detail>, toRoute<Detail>() (типизированные маршруты, как у нас в AppNavHost)✅ компилируется, экран рисуется
«Назад»androidx.compose.ui.backhandler.BackHandler в commonMain✅ компилируется (жест не проверялся)
Схема URLCFBundleURLTypes + onOpenURL в Swift → Kotlin DeepLinks.open⚠️ симулятор показал системный вопрос «Открыть в приложении?», без клика дальше не проверить

3.3 Ловушки, найденные пробами (войдут в чек-лист Э2/Э3)

  1. Info.plist: без CADisableMinimumFrameDurationOnPhone = true Compose падает на старте с внятным текстом (проверка на 120 Гц).
  2. Ресурсы в бандле: при ручной обёртке Xcode (xcodegen + -framework) папка compose-resources/ в приложение не попадает, и любой Res.* роняет экран (MissingResourceException). Штатный путь — фаза сборки Xcode ./gradlew :app:embedAndSignAppleFrameworkForXcode, которая и линкует фреймворк, и копирует ресурсы (в пробе ресурсы скопированы руками из build/generated/compose/resourceGenerator/preparedResources/commonMain/composeResources в <app>/compose-resources/composeResources/<пакет Res>/).
  3. Compose 1.12: layer.record(size) внутри drawWithCache не компилируется — нужна форма layer.record(this, layoutDirection, size); такая строка есть в GlassBackdrop.kt (два места).
  4. nativeCanvas устарел с 1.11 — на iOS имя skiaCanvas; в 1.12 старые имена объявлены ошибкой по релиз-нотам (в пробе — предупреждение).
  5. Kotlin/Native: вызовы Foundation (NSFileManager.URLForDirectory) требуют @OptIn(ExperimentalForeignApi::class).
  6. Gradle 8.13 для Kotlin 2.4.20 помечен устаревшим (предупреждение) — поднять до 8.14+ при Э1.
  7. Xcode 27 (14.09.2026) Kotlin/Native не поддерживает — мак не обновлять.

3.4 Что проверить не удалось (и когда проверится)

3.5 Проба 3: Android в том же KMP-модуле, матрица версий

Та же болванка получила Android-таргет: com.android.application + kotlin.multiplatform в одном модуле, флейворы local/dusha с BuildConfig, плагин Sentry 6.22 с инструментацией Logcat, Paparazzi 1.3.5 со снимком 1170×2532 через Res-ресурсы.

Первый прогон (всё новейшее: Compose Multiplatform 1.12.1, Coil 3.6.3, lifecycle 2.11.0, Ktor 3.6, AGP 8.7.3, compileSdk 35) — отказ на проверке метаданных AAR: androidx.compose.*:1.12.1 и lifecycle-*:2.11.0 «requires Android Gradle plugin 9.1.0 or higher»; coil3:*:3.6.3, qrose-android:1.3.0, okhttp-android:5.5.0 (его тянет Ktor 3.6) — «compile against version 37 or later of the Android APIs». Платформы android-37 в SDK-репозитории для наших cmdline-tools нет (android-36 поставился).

Матрица (checkDushaDebugAarMetadata, AGP 8.7.3):

НаборcompileSdkИтог
CMP 1.11.1 + Coil 3.4.0 + nav 2.9.1 + lifecycle 2.9.4 + qrose 1.0.1 + Ktor 3.635только okhttp-android:5.5.0 требует 37
CMP 1.10.3 + Coil 3.3.0 + nav 2.9.0 + lifecycle 2.9.2 + qrose 1.0.1 + Ktor 3.635то же
CMP 1.9.3 + Coil 3.3.0 + … + Ktor 3.635то же
CMP 1.11.1 + … + Ktor 3.636то же (OkHttp 5.5 хочет именно 37)
CMP 1.11.1 + Coil 3.4.0 + nav 2.9.1 + lifecycle 2.9.4 + qrose 1.0.1 + Ktor 3.2.335проходит (rc=0)

То есть сами Compose 1.9–1.11 на Android с AGP 8.7.3 и compileSdk 35 совместимы; требование AGP 9.1 появилось только у Compose 1.12 (и lifecycle 2.11).

Полная сборка набора «Б» (assembleDushaDebug, recordPaparazziDushaDebug, linkDebugFrameworkIosSimulatorArm64) — три захода:

  1. AGP 8.7.3, Paparazzi 1.3.5, JDK 17: APK composeApp-dusha-debug.apk (37 МБ) и iOS-фреймворк собрались, плагин Sentry отработал (задачи generateSentryBuildTimeOptions, injectSentryDebugMetaPropertiesIntoAssets), но снимок Paparazzi упал: NoSuchMethodError: android.view.View.setRequestedFrameRate(float) — layoutlib в Paparazzi 1.3.5 (январь 2025) не знает метод Android 15, который зовёт Compose 1.11 при отрисовке. То есть Paparazzi 1.3.5 несовместим с Compose 1.11 независимо от KMP — обновление Compose само по себе требует нового Paparazzi.
  2. Paparazzi 2.0.0-alpha05 (JDK 21) на AGP 8.7.3: отказ при применении плагина (GradleVersion.parse — конфликт библиотек Android Tools); ему нужен AGP 8.13.
  3. AGP 8.13.2 + Paparazzi 2.0.0-alpha05 + JDK 21 + CMP 1.11.1 + Ktor 3.2.3 + Coil 3.4.0, compileSdk 35: BUILD SUCCESSFUL за 1 мин 26 с — APK 37 МБ, снимок proto_ProtoScreenshotTest_kit.png записан (462×1000, как и сейчас режет Paparazzi), на нём русская строка из ресурсов Compose с числительным и иконка из .fig. В снимке нужен штатный приём Compose Multiplatform — обернуть содержимое в CompositionLocalProvider(LocalInspectionMode provides true) { PreviewContextConfigurationEffect(); … }, иначе ресурсы не находят Android-контекст (MissingResourceException).

Тот же набор на iOS: фреймворк 1.11.1 собран, приложение запущено в симуляторе — экран пробы 2 идентичен снимку на 1.12.1, DataStore показал «запусков 2» (данные пережили переустановку между запусками).

Попутно: BackHandler для общего кода на Android живёт в отдельном артефакте org.jetbrains.compose.ui:ui-backhandler (на iOS входит в compose.ui), и с 1.11 он объявлен устаревшим в пользу NavigationEventHandler — учесть при переносе девяти вызовов.

Проба 1 в симуляторе iPhone 17e: размытие, стекло, режимы наложения на Skia, контур, поле ввода, список
Проба 1 (Compose Multiplatform 1.12.1): движок теней и наложений на Skia, стекло, навигация.
Проба 2 в симуляторе: SF Pro из байтов, иконки из .fig, QR, картинка по сети, строки с числительными, даты, ответ стенда 200, DataStore, инсеты
Проба 2 на наборе дороги Б (1.11.1): шрифты, иконки, строки, даты, Ktor к стенду, DataStore, Coil, QR, инсеты.
Снимок Paparazzi 2.0 из KMP-модуля: русская строка из ресурсов Compose и иконка
Проба 3: снимок Paparazzi 2.0.0-alpha05 в том же модуле (ресурсы Compose, числительное, иконка из .fig).

4Целевая структура проекта: две дороги

4.1 Дорога Б — «Android как есть» (рекомендуется сейчас)

Один модуль app остаётся Android-приложением и получает iOS-таргеты — так собрана проба 3. Структура модулей, флейворы, BuildConfig, compileSdk 35, плагин Sentry и Gradle 8.13 не меняются; меняются AGP 8.7.3 → 8.13.2 (та же линия, без запрета на один модуль), Paparazzi 1.3.5 → 2.0.0-alpha05 (иначе не рисует Compose 1.11) и JDK для Gradle 17 → 21 (требование Paparazzi 2.0; на макмини поставлен ~/jdk21, сервер сборку и так не тянет). Набор библиотек «весны–лета 2026»: Compose Multiplatform 1.11.1 (Compose 1.11 на Android, 13.05.2026), Ktor 3.2.3 (OkHttp 4.12), Coil 3.4.0, navigation 2.9.1, lifecycle 2.9.4, qrose 1.0.1 (или ZXing на Android и qrose только на iOS), kotlinx-datetime 0.8, DataStore 1.2.1. Что теряем против 1.12: iOS-стиль хендлов выделения, LayerOutsets, фиксы swipe-back и Skia m150 — некритично для паритета; догоняется на дороге А.

4.2 Дорога А — «современный стек» (отдельная задача после iOS)

AGP 9.4.1 + Gradle 9.7.1 + JDK 21 + compileSdk 37 (нужны новые cmdline-tools) + Compose 1.12.1 + Coil 3.6.3 + lifecycle 2.11 + Paparazzi 2.0.0-alpha05 (поддерживает новый плагин Android-библиотеки KMP, но требует JDK 21) + Sentry 6.22 (в его журнале есть правки под AGP 9.3–9.5). Структура по требованию AGP 9: shared (общий код, плагин com.android.kotlin.multiplatform.library, блок kotlin { android { … } }, без флейворов и BuildConfig) + app (Android-приложение со встроенным Kotlin AGP 9, флейворы, Sentry, Paparazzi, MainActivity, DesignShotActivity) + iosApp. Это отдельная инфраструктурная миграция с собственной пересъёмкой; смешивать её с переносом на iOS не стоит.

Ниже — структура для дороги Б.

android-events/                       — имя папки не меняется: на него завязаны tools/, macbuild, спека
  app/
    build.gradle.kts                  — плагины: com.android.application + kotlin.multiplatform +
                                        compose + serialization + paparazzi + sentry (как в пробе 3)
    src/commonMain/kotlin/ru/podushe/app/…      — весь UI, состояния, репозитории, DTO (было src/main/java)
    src/commonMain/composeResources/{drawable,font,values,files}   — 477 картинок, 6 шрифтов, strings.xml, policy/terms.md
    src/androidMain/kotlin/…          — MainActivity, PodusheApp, AppContainer(Android), actual-реализации
    src/androidMain/res/              — mipmap-иконки, themes.xml, xml/file_paths.xml
    src/androidDebug/…                — DesignShotActivity, network_security_config, pic_selfie_preview*
    src/androidUnitTest/…             — все нынешние тесты + Paparazzi (было src/test)
    src/iosMain/kotlin/…              — actual-реализации для iOS, MainViewController
  iosApp/
    project.yml                       — xcodegen: цель PoDushe, конфигурации Dusha/Dushalove/Stage/Prod,
                                        фаза «Compile Kotlin» = ./gradlew :app:embedAndSignAppleFrameworkForXcode
    PoDushe/{iOSApp.swift, Bridges/…, Info.plist, PrivacyInfo.xcprivacy, Assets.xcassets}
    Package.swift или SPM в project.yml — MapLibre, Sentry Cocoa (раздел 5.4)
  tools/macios, tools/shoot_design.py --backend ios   — сборка и съёмка (раздел 7)

Что остаётся ровно как сейчас на Android: applicationId и суффиксы флейворов, BuildConfig-поля (в androidMain), Theme.Podushe, FileProvider, deep links podushe://, задачи :app:assembleDushaDebug, :app:lintDushaDebug, :app:testDushaDebugUnitTest, :app:recordPaparazziDushaDebug, скрипты build.sh, tools/macbuild, tools/shoot_design.py. Правило К-10 спеки не меняется.

Версии (дорога Б): Kotlin 2.4.20; Compose Multiplatform 1.11.1 (на Android — артефакты Google androidx.compose.*:1.11.x); navigation 2.9.1 и lifecycle 2.9.4 от JetBrains (на Android делегируют в androidx); AGP 8.13.2, compileSdk 35, Gradle 8.13 (ровно та версия, которую требует AGP 8.13; предупреждение Kotlin 2.4 об устаревании терпимо), JDK 21 для Gradle, Paparazzi 2.0.0-alpha05, Sentry-плагин 6.22.0.

Конфигурация стендов: на Android — BuildConfig как сейчас; на iOS — ключи Info.plist по конфигурации Xcode (PD_API_BASE_URL, PD_CHATS_BASE_URL, PD_CHAT_WS_URL, PD_ENV_NAME, PD_SENTRY_DSN), которые читает actual объект AppConfig. Один фреймворк на все схемы, стенд выбирается схемой Xcode — для TestFlight это удобнее BuildKonfig (не нужно пересобирать Kotlin под каждый стенд). GIT_SHA на iOS — тоже через Info.plist, его пишет xcodegen/скрипт сборки.

5Правила переноса по классам

5.1 Класс О (39 файлов) — без правок

Модели без дат, чистые состояния экранов, большая часть кита (Tokens.kt, Shapes.kt, Badges.kt, Chips.kt, TabBar.kt, …), валидаторы, FigVec.kt. Список — приложение А.

5.2 Класс М (105 файлов) — механические правила

ПравилоКакГде особенно
R.drawable.x → Res.drawable.x, painterResource(R…) → painterResource(Res…), @DrawableRes Int → DrawableResourceскрипт замены + импорты ru.podushe.app.res.*; PdImage.Res(id: Int) становится PdImage.Res(res: DrawableResource); ImageBitmap.imageResource(id) (растянутые растры с FilterQuality.None) → imageResource(Res.drawable.x) из Compose Resources148 файлов; Picture.kt, DesignShots
R.string.x → Res.string.x, stringResource тот же; @StringRes Int → StringResource; R.plurals → Res.pluralsскрипт; 1 542 строки strings.xml переносятся файлом как есть (формат подстановок %1$s/%1$d тот же, проверено пробой 2)Formats.kt, PermissionGate.kt (enum держит id строк), AppNotification-типы
Strings/rememberStrings() (23 файла)интерфейс остаётся; реализация без Resources: при старте приложения все строки и числительные читаются в память (Res.allStringResources + getString, миллисекунды), Strings.get остаётся синхронным; подстановки — свой formatArgs для %1$s/%1$d (69 строк с подстановками)Formats.kt, AppViewModels.kt
java.time → kotlinx-datetime 0.8OffsetDateTime → kotlin.time.Instant (+ TimeZone.currentSystemDefault() при показе); LocalDate, LocalDateTime, YearMonth → kotlinx.datetime.LocalDate, LocalDateTime, YearMonth; DateTimeFormatter.ofPattern("d MMMM", RU) → LocalDate.Format { day(Padding.NONE); char(' '); monthName(RU_GENITIVE) }; TextStyle.FULL/SHORT_STANDALONE для месяцев и дней → свои таблицы (родительный «сентября», именительный «Сентябрь», короткий «сен», дни «четверг»/«чт»); Duration.between → Instant - Instant; OffsetDateTime.parse → Instant.parse (ISO с зоной — тот же формат стенда)Formats.kt (13 функций), Calendar.kt, Mapping.kt, Models.kt, EventsRepository.kt, состояния афиши/уведомлений/чата, DayGroups.kt; тесты FormatsTest, DayGroupsTest, ChatFeedTest, MappingTest переписываются на те же типы
Locale("ru")исчезает вместе с форматтерами; lowercase(RU) → lowercase() (для кириллицы без локали одинаково)13 файлов
String.format(Locale.US, "%.1f", km) и "%02d".format(x)нет в Kotlin/Native → две функции в Formats: fixed(km, 1) (умножение, округление, разделитель «.») и pad2(x); "%.5f, %.5f" координат — fixed(lat, 5)Formats.kt (4), CreateEventViewModel, CreateFormRoute, CreateSheets, AboutForm
System.currentTimeMillis() → Clock.System.now().toEpochMilliseconds(); System.nanoTime() (id снекбара/события) → TimeSource.Monotonic.markNow() или счётчикSnackbar.kt, AppViewModels.kt, PhotoPicker, SelfieCameraHost, ProfileRepository
UUID.randomUUID() → kotlin.uuid.Uuid.random(); android.util.Base64 (JWT-клейм в TokenStore) → kotlin.io.encoding.Base64.UrlSafeChatSocket, Telemetry, TokenStore
Math.toRadians/round → kotlin.mathGeo.kt, EventsRepository.kt
android.util.Log → Telemetry.log (уже есть общий фасад)9 файлов, 45 вызовов в AppViewModels.kt
BuildConfig.* → AppConfig.* (expect)8 файлов
LocalContext.current для Coil → LocalPlatformContext.current (Coil 3); для resources → Strings; для файлов/разрешений — PdPlatform13 файлов
IOException → kotlinx.io.IOException (Ktor использует его же)8 файлов
@Volatile → kotlin.concurrent.Volatile; synchronized(lock) → Mutex/атомикInterestLabels, Telemetry, EventMap (httpTuned)
androidx.activity.compose.BackHandler → androidx.compose.ui.backhandler.BackHandler из артефакта org.jetbrains.compose.ui:ui-backhandler (9 мест; с 1.11 помечен устаревшим в пользу NavigationEventHandler)AppNavHost (7), CreateFormRoute, AboutStepScreen
TimeUnit.SECONDS → kotlin.time.Duration (уйдёт с OkHttp)сетевой слой

5.3 Класс Б (11 файлов) — замена библиотек

Сеть (ApiClient.kt, PodusheApi.kt, ChatsApi.kt, ServerError.kt, AuthRepository.kt, HttpJournalInterceptor.kt, PlaceSearch.kt, ChatSocket.kt): Ktor 3.2.3 (на Android движок OkHttp 4.12 — тот же, что сейчас; 3.6 тянет OkHttp 5.5 с требованием compileSdk 37). Retrofit-интерфейс на 46 ручек становится классом с 46 функциями Ktor (client.get("public/events") { parameter("limit", 50) }.body<…>()); две базы (API_BASE_URL, CHATS_BASE_URL) — два HttpClient с общим движком; Bearer — плагин defaultRequest/Auth с собственной логикой обновления: тот же мьютекс, тот же «повторить, если сосед уже обновил», тот же отдельный «голый» клиент для refresh — переносится один в один, только без runBlocking (плагин HttpSend асинхронный); retrofit2.HttpException → ResponseException (в serverDetail() и AuthRepository.reason() читается response.bodyAsText()); multipart (verifyPhoto, uploadProfilePhoto, чат-медиа — 5 мест) → MultiPartFormDataContent(formData { append("photo", bytes, Headers { ContentType, filename }) }); retrofit2.Response<Unit> → HttpResponse; таймауты connect 10 / read 20 / write 60 → плагин HttpTimeout; Photon в PlaceSearch — тот же клиент; WebSocket чата → client.webSocketSession(url) { header("Authorization", …) }, pingInterval = 25.seconds у движка (Darwin и OkHttp поддерживают), incoming → тот же SharedFlow; журнал отказов (HttpJournalInterceptor) → плагин HttpResponseValidator/ResponseObserver с чтением тела до 4 КБ; крошки Sentry для запросов — свой плагин (Sentry KMP их не даёт).

Картинки (Picture.kt, AfishaThumbnails.kt, PodusheApp.kt, EventMap.kt, экраны с AsyncImage): Coil 3.6.3: coil3.compose.AsyncImage/SubcomposeAsyncImage — те же параметры; Interceptor → coil3.intercept.Interceptor (chain.request.data, chain.size, coil3.size.Dimension.Pixels, Scale) — AfishaThumbnails переносится почти дословно; ImageLoaderFactory → SingletonImageLoader.Factory в PodusheApp (Android) и setSingletonImageLoaderFactory на iOS, движок KtorNetworkFetcherFactory (лимит 12 соединений на хост — настройка движка Ktor); allowHardware(false) — только Android; .target(onSuccess = { drawable -> … bitmap }) в маркерах карты → coil3.Image → toBitmap() (Android) / org.jetbrains.skia.Bitmap (iOS); placeholder = painterResource(id) → painterResource(Res.drawable.x); SVG — coil-svg тот же артефакт.

QR (QrCode.kt): qrose 1.3.0 даёт Painter; наши требования — цвет чернил #342D35, уровень коррекции H (логотип в центре), поле 0 — выставляются QrOptions (errorCorrectionLevel = High, padding = 0, цвет). Сетка модулей та же (стандарт QR), но перерисовку Canvas-ом по матрице, как сейчас, qrose не даёт — кадры 688:61306 и 348:47357 пересверить (в макете код нарисован контуром и всё равно «ведёт в никуда», Р-…).

Журнал (Telemetry.kt): на Android остаётся Sentry Android 8.57 с плагином (инструментация Log.*, mapping) — файл целиком уходит в androidMain как actual object Telemetry; общий expect object Telemetry { log/info/warn/error/ setUser/setScreen } — тот же API, который уже зовут 9 файлов. iOS — раздел 5.4.

DataStore (TokenStore, OnboardingFlags, RecentQueries): делегат Context.preferencesDataStore → PreferenceDataStoreFactory.createWithPath { PdPlatform.dataFile("auth.preferences_pb") } (проверено пробой 2). Токены на iOS могут жить в Keychain (multiplatform-settings 1.3) — решение при Э3: DataStore-файл в песочнице приложения iOS зашифрован системой (Data Protection), для первой версии достаточно.

5.4 Класс П (35 файлов) — что и как пишется в двух вариантах

Общий приём: в commonMain объявляется фасад, Android-реализация — это нынешний код, перенесённый в androidMain почти без правок; iOS-реализация — новая. Файлы-экраны при этом остаются в commonMain, из них уходят только платформенные строки.

УзелФайлы (строк платформенных)Фасад в commonMainAndroid (androidMain)iOS (iosMain / Swift)
Нативный канвасFigDraw.kt (весь), Buttons.kt (4 места), Frame.kt (1), 8 экранов-кадров (по 5–15 строк), EventCards.kt (1)PdCanvasOps: saveLayer(rect, alpha), save/restore/translate/clipPath, drawPath(path, PdPaint), drawOval, drawLayer(PdBlurLayer, PdPaint); PdPaint (цвет, альфа, PdBlend PLUS/DST_IN/DST_OUT/COLOR_DODGE, линейный градиент, обводка, размытие); PdBlurLayer (битмап ALPHA_8 + origin + down); контуры остаются androidx.compose.ui.graphics.Path и конвертируются внутри реализаций (asAndroidPath() / asSkiaPath())как сейчас: android.graphics.Canvas/Paint/BlurMaskFilter/BitmapShader/Matrix, LruCacheorg.jetbrains.skia.Canvas/Paint/Path/Bitmap(ALPHA_8)/MaskFilter.makeBlur(σ = R/2 напрямую)/Shader.makeLinearGradient/BlendMode; свой LRU на LinkedHashMap с бюджетом 24 МБ; треугольное ядро на FloatArray — общий код (Bitmap.readPixels/installPixels вместо copyPixelsToBuffer). figGlassRim, figFills, figInnerShadow, figDropShadow — через фасад
Разбор цвета #RRGGBBEventCards.kt:227 (android.graphics.Color.parseColor)своя функция hexColor() — 3 строки——
КартаEventMap.kt (647), AppNavHost.kt (15 мест: mapRef, CameraUpdateFactory, LatLng, projection, OnCameraIdleListener), MapNodeScreen, EventPageNodeScreen, EventPageState, MapState, CreateFormRoute (8)PdMap(events, center: PdLatLng, zoom, interactive, textureMode, onEventClick, onMapReady: (PdMapHandle) -> Unit); PdMapHandle: animateTo(lat, lng, zoom, ms), zoomBy, cameraTarget, toScreen/fromScreen, addOnIdle/removeOnIdle; PdMapSnapshot(style, center, zoom, size) для мини-карт DUSHA-71как сейчас (MapLibre Android 11.5.2, символьные слои, маркеры-битмапы, MapSnapshotter, TextureView/SurfaceView)Swift-мост: в iosApp пакет MapLibre iOS 6.31 (SPM), класс MapBridge: PdMapFactory на Swift создаёт MLNMapView, стиль OpenFreeMap с тем же перекрасом слоёв, MLNShapeSource с кластеризацией (radius 64, maxZoom 13), MLNSymbolStyleLayer с iconImageName по свойству, картинки маркеров style.setImage(UIImage, forName:), тап через visibleFeatures(at:), состав кластера leaves(of:), снимки MLNMapSnapshotter. Kotlin получает UIView через UIKitView. Маркеры и стопки рисуются в общем коде на Skia (сейчас — StaticLayout/Paint на Android; на iOS TextLine/Paragraph Skia с SF Pro), результат → PNG-байты → UIImage. Плитки: свой лимит соединений — настройка URLSessionConfiguration
Камера селфиSelfieCameraHost.kt (132), SelfieScreen.kt (превью)PdSelfieCamera(onShot(path), onFailed, front/back, torch/screenFlash) как @Composable слот previewCameraX как сейчасAVCaptureSession + AVCaptureVideoPreviewLayer в UIKitView, AVCapturePhotoOutput → JPEG в NSTemporaryDirectory; фонарь AVCaptureDevice.torchMode; «вспышка экраном» — белый слой Compose поверх на время снимка; переворот — смена AVCaptureDeviceInput
Снимок в прямом положении и сжатиеProfileRepository.kt (UprightBitmap, SelfiePng, ProfileJpeg)PdImages.uprightPng(path, maxSide), uprightJpeg(uri, maxSide, quality) → путь файлакак сейчас (BitmapFactory, ExifInterface)UIImage(contentsOfFile) уже учитывает EXIF-ориентацию при отрисовке в UIGraphicsImageRenderer; UIImagePNGRepresentation/UIImageJPEGRepresentation; фронтальная камера iOS не зеркалит файл — проверить на устройстве
Выбор фото и снимокPhotoPicker.kt (174)PdPhotoPicker: pickFromGallery(), takePhoto(), результат — путь/URI строкой (как сейчас String)PickVisualMedia, TakePicture + FileProviderPHPickerViewController (filter images, limit 1) → loadFileRepresentation → копия в кэш; снимок — UIImagePickerController(.camera) или наш PdSelfieCamera; разрешение галереи не нужно (как и на Android)
РазрешенияPermissionGate.kt (171)PdPermissions.status(p), request(p): Boolean, canAskAgain(p), openSettings()как сейчаскамера AVCaptureDevice.requestAccess(for: .video) + authorizationStatus (denied ⇒ только настройки); геолокация CLLocationManager.requestWhenInUseAuthorization + делегат; уведомления UNUserNotificationCenter.requestAuthorization; настройки UIApplication.open(openSettingsURLString). Шторки макета (732:77193, 732:76379) — общий код
Геолокация и геокодерGeo.kt (123), PlaceSearch.kt (2 функции)PdLocation.current(timeoutMs), lastKnown(); PdGeocoder.search(q, near), reverse(lat, lng) (запасной после Photon)LocationManager, Geocoder как сейчасCLLocationManager (делегат → suspendCancellableCoroutine, desiredAccuracy 100 м), CLGeocoder.geocodeAddressString / reverseGeocodeLocation с Locale("ru_RU"); CityCenters — общий код
Окно, инсеты, диалогиViewport.kt (findActivity, PdEdgeToEdgeDialogWindow, WindowManager), Insets.ktPdViewport — уже общий (BoxWithConstraints); PdEdgeToEdgeDialogWindow → expect, на iOS пусто (диалоги Compose на iOS — слой в том же окне); PodusheInsets.Top → max(54.dp, safeDrawing.top) (17e: 47 → 54; Dynamic Island: 59); Bottom 24; pdNavLiftDp — WindowInsets.navigationBars на iOS = 34 → подъём 10 dp, как на Android с жестовой навигациейкак сейчасLocalWindowInfo.containerSize там, где нужен размер окна
Хранилище, пути, файлыTokenStore, OnboardingFlags, RecentQueries, PolicyScreen (openRawResource), PhotoPicker/SelfieCameraHost (cacheDir)PdPlatform.dataFile(name), cacheFile(name), readText(Res.files); политика/условия → Res.readBytes("files/policy.md")Context.filesDir/cacheDirNSDocumentDirectory/NSTemporaryDirectory
Схема URL и «поделиться»AppNavHost.kt (23 мест Intent: 4 «поделиться», 1 «открыть карту geo:», deep links в MainActivity), EventRegisterStatePdShare.text(text), PdExternal.openMap(lat, lng, title), DeepLinks.incomingIntent.createChooser/ACTION_VIEW geo:UIActivityViewController; карта — maps://?q= (Apple Maps) — решение по Figma/владельцу: на iOS открывать Apple Maps или Яндекс.Карты по URL-схеме; onOpenURL → DeepLinks (проверено до системного вопроса)
ТелеметрияTelemetry.kt (300), MainActivity (withSentryObservableEffect)expect object Telemetry + TelemetryNav (крошки переходов: слушатель NavController в общем коде)как сейчасSwift-мост: Sentry Cocoa 8.x как SPM-пакет в iosApp, класс TelemetryBridge реализует Kotlin-интерфейс TelemetrySink (capture(level, tag, text), breadcrumb, setUser, setTag); Sentry.logger() (Sentry Logs) есть в Cocoa 8.x; dSYM на GlitchTip — sentry-cli upload-dif в скрипте архива. Альтернатива — sentry-kotlin-multiplatform 0.27 (сам линкует Cocoa через SPM), но без Logs и без интеграции Compose; мост проще и полностью под контролем
Точка входа и DIMainActivity.kt, PodusheApp.kt (AppContainer(app: Application))AppContainer(platform: PdPlatform); AppRoot() — то, что сейчас внутри setContent {} (PdViewport { PodusheTheme { AppScaffold { AppNavHost } } })MainActivity зовёт AppRoot(), fixture=1/route= из intentMainViewController(args) читает ProcessInfo.arguments (--fixture 1 --route event --id 1 --screen 158-9505 --h N --y M)
Шрифтыtheme/Type.kt (Font(R.font) ×6)expect val SfProFamily: FontFamily (и Condensed) — не composable, стили PdType/PodusheType остаются valFont(R.font.sf_pro_*) как сейчас (файлы остаются и в res/font, и в composeResources/font — или только в composeResources с Font(Res.font) в композиции — решить на Э0 по итогам пересъёмки)байты из Res.readBytes("files/sf_pro_*.ttf") при старте → androidx.compose.ui.text.platform.Font(id, bytes, weight) (проверено пробой 2: веса 400/510/590/650/700/760 попадают в свои файлы)
Устройство в журналеTelemetry (Build.MANUFACTURER/MODEL/VERSION)PdPlatform.deviceInfo()как сейчасUIDevice.model, systemVersion

5.5 Класс Д (33 файла, 7 074 строки) — кадры съёмки

DesignShots.frames (623 записи guid → composable) и *Frames.kt ссылаются на R.drawable-фикстуры, DesignFixture и внутренние экраны — после замены R → Res это обычный общий код. Решение: перенести их в commonMain в пакет ru.podushe.app.design (в релизном APK их выбросит R8, в iOS-фреймворке они останутся — это допустимо на время Э3–Э4; вынести в отдельный модуль :shots можно позже, когда сверка на iOS закончится). DesignShotActivity → androidDebug; на iOS её роль выполняет аргумент запуска --screen (раздел 7). Растры pic_selfie_preview* из debug/res → composeResources (2 файла).

5.6 Класс Т (18 файлов) — тесты

Переезжают в androidUnitTest без изменений смысла; тесты дат переписываются на kotlinx-datetime вместе с Formats. Paparazzi — по итогам пробы 3 (3.5). FakeStrings (тестовая реализация Strings с String.format) — на JVM может остаться, но лучше сразу на общий formatArgs.

6Ресурсы

РесурсСейчасКудаОсобенности
Векторные иконкиres/drawable/*.xml — 108 файлов, только <vector>/<path> (проверено: ни <gradient>, ни @color, ни ?attr)composeResources/drawable/поддерживаются как есть; в пробе 2 три из них нарисовались с подкраской
Растрыdrawable-nodpi 217 файлов (104 МБ, картинки макета pic_*), drawable-hdpi/xhdpi/xxhdpi/xxxhdpi 141 файл (13 МБ)composeResources/drawable/ (nodpi → без квалификатора, остальные — drawable-xxhdpi и т. д., квалификаторы плотности поддерживаются)ImageBitmap.imageResource для растров с FilterQuality.None (Picture.kt) — есть в Compose Resources; размер бандла iOS вырастет на те же 117 МБ — отдельная задача сжатия после паритета
Шрифтыres/font/sf_pro_*.ttf 6 файлов, 1,5 МБcomposeResources/font/ и (для загрузки байтами) composeResources/files/см. 5.4 «Шрифты»
Строкиres/values/strings.xml 1 542 строки, 7 plurals, 69 с подстановкамиcomposeResources/values/strings.xmlформат тот же; \n, кавычки, «ё» — как есть; проверка: grep спеки К-9 остаётся
Документыres/raw/policy.md, terms.mdcomposeResources/files/Res.readBytes
Иконка приложенияmipmap-*Android — как есть; iOS — AppIcon.appiconset 1024 из того же источника (Figma)—
Тема, FileProvider, network securityvalues/themes.xml, xml/file_paths.xml, debug/res/xml/network_security_config.xmlостаются в androidMain/res и androidDebug/resна iOS аналог network security — NSAppTransportSecurity только для local (http) в debug-конфигурации

7Сборка и съёмка на iOS

8Порядок работ с контрольными точками

Ветка feature/ios-cmp от release/2026-09-23 (по правилу «своя ветка на каждую работу»); Android-сборки для тестировщиков продолжают выходить из релизной ветки. Контрольная точка («ворота») каждого шага — то, что должно быть зелёным, прежде чем шаг считается закрытым.

ШагЧтоВорота
Э0.1Фасады: PdPlatform, PdCanvasOps/PdPaint/PdBlurLayer (FigDraw и 10 файлов канваса), PdMap*, PdSelfieCamera, PdPhotoPicker, PdPermissions, PdLocation/PdGeocoder, PdImages, PdShare, AppConfig, Telemetry как expect — реализации пока только Android, файлы на местесборка, юнит-тесты, пересъёмка кадров затронутых экранов (кит, 8 экранов с арт-слоями, карта) — diff без изменений
Э0.2Строки и ресурсы: strings.xml и картинки в composeResources, R.* → Res.*, Strings на предзагрузке, шрифты (5.4)полная пересъёмка (шрифты и картинки касаются всех кадров)
Э0.3Даты: kotlinx-datetime в Formats, Calendar, моделях; тесты датюнит-тесты дат; кадры афиши, чата, уведомлений, календаря, формы события
Э0.4Сеть: Ktor вместо Retrofit/OkHttp; Coil 3; qrose; DataStore по путиручной проход основных сценариев на стенде dusha (вход, афиша, событие, запись, чат с фото, профиль с фото, селфи); кадры QR
Э0.5Мелочи класса М: String.format, System.*, UUID, Log, BackHandlerсборка, тесты
Э1Kotlin 2.0.21 → 2.4.20, Compose 1.7.6 → 1.11 (через Compose Multiplatform 1.11.1), navigation 2.9.1, lifecycle 2.9.4, AGP 8.7.3 → 8.13.2, Paparazzi 1.3.5 → 2.0.0-alpha05 (снимки — с PreviewContextConfigurationEffect), JDK 21 на маке; Gradle 8.13 и compileSdk 35 без изменений (дорога Б)полная пересъёмка 623 кадров, Paparazzi, ручной проход; расхождения текста разбираются до зелёного или записываются в реестр
Э2Переезд в KMP одним коммитом в окно тишины: src/main/java → src/commonMain/kotlin, androidMain, androidDebug, androidUnitTest, iOS-таргеты, iosApp (xcodegen, фаза Gradle, Info.plist с CADisableMinimumFrameDurationOnPhone, PrivacyInfo, схема URL, тексты разрешений, иконка), tools/maciosAndroid: APK байт-в-байт по содержимому классов не гарантируется, поэтому — полная пересъёмка + ручной проход; iOS: приложение запускается до велкома на симуляторе
Э3iosMain + Swift-мосты: канвас на Skia, карта, камера, фото, разрешения, гео, окно, шаринг, телеметрия, шрифты, съёмка по аргументамкаждый узел — ручная проверка в симуляторе; карта и камера — на живом iPhone
Э4Сверка 623 кадров на iOS по десяткам S01…S95, разбор красных, ручной проходREPORT-ios с теми же четырьмя числами на кадр
Э5TestFlight: App ID, запись в ASC, профили, архив, загрузка, внутренние тестерысборка стоит в TestFlight у команды

Перед Э1 и Э2 — метка backup/… и прирост бандла на мак (раздел 1).

9Риски после разбора

РискОценка после разбораЧем снимаем
Android-сборка «поедет» при обновлении Compose 1.7.6 → 1.11реальный: релиз-ноты 1.8–1.11 называют убранный неявный слой у текста с режимами наложения, тихую подмену незагрузившегося шрифта, пересчёт размера полей; оптимизация BasicText при softWrap=false — уже в 1.12, на дороге Б не затрагиваетЭ1 отдельно, полная пересъёмка, разбор до зелёного
Paparazzi1.3.5 не рисует Compose 1.11 (проба 3); 2.0.0-alpha05 работает, но это alpha и требует AGP 8.13 + JDK 21принято: Paparazzi 2.0.0-alpha05; если alpha подведёт — снимки только через эмулятор (основной конвейер сверки и так там)
Замороженный стек дороги БCompose 1.11 и Ktor 3.2 — версии 2026 года, но не последние; следующий шаг Android (Compose 1.12+) потребует AGP 9 и разделения модулейдорога А отдельной задачей после iOS; до неё библиотеки не обновлять
MapLibre iOS: биндинг из Kotlinснят Swift-мостом (Kotlin не линкует бинарный xcframework напрямую)Э3, проверка на устройстве
Sentry на iOSснят Swift-мостом; Sentry Logs есть в Cocoa 8.xЭ3
Текст ±1 px на iOSожидаемый (Skia + CoreText против android.text + FreeType)метрика «без текста» + inkcheck; класс остатка только по К-16
Размер iOS-бандла (117 МБ растров макета + фреймворк)известныйсжатие растров отдельной задачей после Э4; релизная сборка со stripping
Параллельные сессии в android-events во время Э0средний: Э0 правит Formats, ApiClient, китмелкие коммиты, ежедневный rebase; объявлять соседям файлы, которые трогаются (правило 22.09)
Окно тишины для Э2нужно 1–2 днядоговориться с владельцем заранее (вопрос 2 в docs/41 §10)

10Решения, принятые в разборе (технические, за исполнителем), и что остаётся за владельцем

Принято здесь: дорога Б (один модуль app, версии из 4.1, включая AGP 8.13.2, Paparazzi 2.0 и JDK 21); папка android-events без переименования; конфигурация iOS через Info.plist; Swift-мосты для MapLibre и Sentry; кадры съёмки в общем коде; статус-бар max(54, системный); токены в DataStore (Keychain — позже); Paparazzi по итогам пробы 3.

За владельцем — вопросы из docs/41, раздел 10: путь общего кода (получено «можно попробовать»), окно тишины для Э2, демо-номер и удаление аккаунта на сервере, юрист по селфи, аккаунт Apple, пуши, карточка в трекере. Новый вопрос от разбора: «Открыть в картах» на iOS — Apple Maps или Яндекс.Карты по схеме URL (на Android открывается geo:-ссылка, её ловит любая карта). Предлагаю Apple Maps, иначе решает макет.

Приложение А. Таблицы по файлам (241 файл)

Приложение А. Таблицы по файлам

Сводка по классам

КлассФайловСтрок
О — как есть394609
М — механическая правка10525922
Б — замена библиотеки111364
П — два варианта (expect/actual)3514309
Д — кадры съёмки (debug)337074
Т — тесты (остаются на JVM)182118

Сколько файлов затрагивает каждый признак

ПризнакФайлов
R.drawable/R.string148
java.time38
Context как параметр24
канвас android17
Locale13
LocalContext13
сеть Retrofit/OkHttp12
Coil 29
Log9
активность/окно6
BuildConfig6
файлы/потоки6
String.format6
System.*5
Sentry Android4
IOException4
@Volatile/synchronized4
TimeUnit4
интенты/Uri4
DataStore(Context)3
UUID/Base643
Math.*3
потоки/Executor3
Build.VERSION3
карта MapLibre3
Resources/getString3
BackHandler(activity)3
runBlocking2
разрешения2
AndroidView2
лаунчеры2
LruCache1
ZXing1
камера1
Font(R.font)1

Файлы класса П (два варианта)

ФайлСтрокПлатформенные признакиБиблиотекиМеханика
main/java/ru/podushe/app/MainActivity.kt96активность/окноSentry AndroidBuildConfig
main/java/ru/podushe/app/PodusheApp.kt108активность/окносеть Retrofit/OkHttp, Coil 2—
main/java/ru/podushe/app/data/auth/TokenStore.kt55Context как параметрDataStore(Context)UUID/Base64
main/java/ru/podushe/app/data/geo/Geo.kt123разрешения, потоки/Executor, Context как параметр—Math.*
main/java/ru/podushe/app/data/geo/PlaceSearch.kt253Context как параметр, Build.VERSIONсеть Retrofit/OkHttpLocale, Log, BuildConfig, IOException, TimeUnit
main/java/ru/podushe/app/data/prefs/OnboardingFlags.kt60Context как параметрDataStore(Context)—
main/java/ru/podushe/app/data/prefs/RecentQueries.kt44Context как параметрDataStore(Context)—
main/java/ru/podushe/app/data/repo/ProfileRepository.kt237канвас android, интенты/Uri, файлы/потоки, Context как параметрсеть Retrofit/OkHttpjava.time, System.*, IOException
main/java/ru/podushe/app/data/telemetry/Telemetry.kt300активность/окно, потоки/Executor, Context как параметр, Build.VERSIONSentry AndroidUUID/Base64, Log, BuildConfig, @Volatile/synchronized
main/java/ru/podushe/app/ui/components/ChatComponents.kt262Context как параметрCoil 2R.drawable/R.string, LocalContext
main/java/ru/podushe/app/ui/components/EventCards.kt559канвас android, Context как параметрCoil 2R.drawable/R.string, LocalContext
main/java/ru/podushe/app/ui/components/EventMap.kt647канвас android, AndroidView, карта MapLibre, Context как параметрсеть Retrofit/OkHttp, Coil 2R.drawable/R.string, LocalContext, Resources/getString, @Volatile/synchronized, TimeUnit
main/java/ru/podushe/app/ui/components/People.kt240Context как параметрCoil 2LocalContext
main/java/ru/podushe/app/ui/format/Formats.kt359Context как параметр—R.drawable/R.string, java.time, Locale, String.format, LocalContext, Resources/getString
main/java/ru/podushe/app/ui/kit/Buttons.kt887канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/kit/FigDraw.kt361канвас android, файлы/потоки, LruCache——
main/java/ru/podushe/app/ui/kit/Frame.kt483канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/kit/Viewport.kt225активность/окно, Context как параметр—LocalContext
main/java/ru/podushe/app/ui/navigation/AppNavHost.kt2441канвас android, интенты/Uri, карта MapLibre, файлы/потоки, Context как параметр—R.drawable/R.string, java.time, BuildConfig, LocalContext, Resources/getString, BackHandler(activity)
main/java/ru/podushe/app/ui/permissions/PermissionGate.kt171интенты/Uri, разрешения, лаунчеры, Context как параметр, Build.VERSION—R.drawable/R.string, LocalContext
main/java/ru/podushe/app/ui/screens/afisha/EventCoverArt.kt172канвас android, Context как параметр—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/EventPageNodeScreen.kt1263канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/MapNodeScreen.kt892канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/auth/WelcomeNodeScreen.kt424канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/CreateFormRoute.kt424канвас android, карта MapLibre, Context как параметр—R.drawable/R.string, java.time, Locale, String.format, LocalContext, BackHandler(activity)
main/java/ru/podushe/app/ui/screens/create/OnboardingArt.kt308канвас android, Context как параметр—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/legal/PolicyScreen.kt48файлы/потоки, Context как параметр—R.drawable/R.string, LocalContext
main/java/ru/podushe/app/ui/screens/profile/ProfileNodeScreen.kt1533канвас android, Context как параметр—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/CityNodeScreen.kt436канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/PhotoPicker.kt174интенты/Uri, лаунчеры, файлы/потоки, Context как параметр—R.drawable/R.string, System.*, LocalContext
main/java/ru/podushe/app/ui/screens/registration/SelfieCameraHost.kt132активность/окно, AndroidView, камера, файлы/потоки, потоки/Executor, Context как параметр—System.*, Log, LocalContext
main/java/ru/podushe/app/ui/screens/registration/SelfieNodeScreen.kt213канвас android—R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/SelfieScreen.kt90Context как параметрCoil 2R.drawable/R.string, LocalContext
main/java/ru/podushe/app/ui/screens/registration/SuccessNodeScreen.kt183канвас android, Context как параметр—R.drawable/R.string
main/java/ru/podushe/app/ui/theme/Type.kt106Font(R.font)—R.drawable/R.string

Файлы класса Б (замена библиотеки)

ФайлСтрокБиблиотекиМеханика
main/java/ru/podushe/app/data/api/ApiClient.kt156сеть Retrofit/OkHttp, Sentry AndroidLog, BuildConfig, IOException, @Volatile/synchronized, runBlocking, TimeUnit
main/java/ru/podushe/app/data/api/ChatsApi.kt68сеть Retrofit/OkHttp—
main/java/ru/podushe/app/data/api/PodusheApi.kt216сеть Retrofit/OkHttp—
main/java/ru/podushe/app/data/api/ServerError.kt33сеть Retrofit/OkHttp—
main/java/ru/podushe/app/data/auth/AuthRepository.kt89сеть Retrofit/OkHttp—
main/java/ru/podushe/app/data/chat/ChatSocket.kt93сеть Retrofit/OkHttpUUID/Base64, Log, BuildConfig, runBlocking, TimeUnit
main/java/ru/podushe/app/data/images/AfishaThumbnails.kt59Coil 2—
main/java/ru/podushe/app/data/telemetry/HttpJournalInterceptor.kt66сеть Retrofit/OkHttp, Sentry AndroidIOException
main/java/ru/podushe/app/ui/kit/Picture.kt132Coil 2—
main/java/ru/podushe/app/ui/kit/QrCode.kt56ZXing—
main/java/ru/podushe/app/ui/screens/registration/RegistrationViewModel.kt396сеть Retrofit/OkHttpjava.time, Log

Файлы класса М (механическая правка)

ФайлСтрокЧто
main/java/ru/podushe/app/data/api/Mapping.kt225java.time
main/java/ru/podushe/app/data/events/EventsRepository.kt639java.time, Locale, Math.*
main/java/ru/podushe/app/data/mock/DesignFixtureRepository.kt118java.time
main/java/ru/podushe/app/data/mock/MockEvents.kt487R.drawable/R.string, java.time
main/java/ru/podushe/app/data/model/InterestCategories.kt49R.drawable/R.string
main/java/ru/podushe/app/data/model/InterestLabels.kt32@Volatile/synchronized
main/java/ru/podushe/app/data/model/Models.kt284java.time
main/java/ru/podushe/app/data/validation/Validators.kt90java.time
main/java/ru/podushe/app/ui/AppViewModels.kt1656R.drawable/R.string, java.time, System.*, Log
main/java/ru/podushe/app/ui/ChatViewModel.kt174java.time, Log
main/java/ru/podushe/app/ui/CreateEventViewModel.kt254R.drawable/R.string, Locale, String.format, Log
main/java/ru/podushe/app/ui/components/AfishaBlocks.kt220R.drawable/R.string
main/java/ru/podushe/app/ui/components/AppIcons.kt75R.drawable/R.string
main/java/ru/podushe/app/ui/components/Calendar.kt229java.time, Locale
main/java/ru/podushe/app/ui/components/Fields.kt353R.drawable/R.string
main/java/ru/podushe/app/ui/components/Scaffold.kt441R.drawable/R.string
main/java/ru/podushe/app/ui/components/Sheets.kt343R.drawable/R.string
main/java/ru/podushe/app/ui/components/Snackbar.kt172System.*
main/java/ru/podushe/app/ui/components/TabBar.kt157R.drawable/R.string
main/java/ru/podushe/app/ui/debug/UiKitGallery.kt459R.drawable/R.string, java.time
main/java/ru/podushe/app/ui/format/DayGroups.kt34java.time
main/java/ru/podushe/app/ui/format/EventLabels.kt40R.drawable/R.string
main/java/ru/podushe/app/ui/kit/Cards.kt752R.drawable/R.string
main/java/ru/podushe/app/ui/kit/Fields.kt515R.drawable/R.string
main/java/ru/podushe/app/ui/kit/Overlays.kt513R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/AfishaChips.kt38R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/AfishaPageNodeScreen.kt352R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/AfishaPageState.kt84R.drawable/R.string, java.time, Locale
main/java/ru/podushe/app/ui/screens/afisha/CalendarSheetNodeScreen.kt193R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/CategoryPageNodeScreen.kt323R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/CategoryState.kt70R.drawable/R.string, java.time
main/java/ru/podushe/app/ui/screens/afisha/ChatNodeScreen.kt586R.drawable/R.string, Math.*
main/java/ru/podushe/app/ui/screens/afisha/CityAfishaNodeScreen.kt318R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/CityAfishaState.kt115R.drawable/R.string, java.time, Locale
main/java/ru/podushe/app/ui/screens/afisha/CityListNodeScreen.kt293R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/CityListState.kt57R.drawable/R.string, java.time
main/java/ru/podushe/app/ui/screens/afisha/EventMembersNodeScreen.kt254R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/EventMembersState.kt90R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/EventPageState.kt186R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/EventRegisterNodeScreen.kt515R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/EventRegisterState.kt99R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/FilterSheetNodeScreen.kt618R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/FiltersState.kt118R.drawable/R.string, java.time
main/java/ru/podushe/app/ui/screens/afisha/MapState.kt83R.drawable/R.string, java.time
main/java/ru/podushe/app/ui/screens/afisha/PhotoViewerNodeScreen.kt146R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/ReportSheetNodeScreen.kt243R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/ReportSheetState.kt46R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/SearchPageNodeScreen.kt255R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/SearchPageState.kt72R.drawable/R.string
main/java/ru/podushe/app/ui/screens/afisha/SheetStates.kt177R.drawable/R.string, java.time, Locale
main/java/ru/podushe/app/ui/screens/auth/OtpNodeScreen.kt176R.drawable/R.string
main/java/ru/podushe/app/ui/screens/auth/OtpScreen.kt108R.drawable/R.string
main/java/ru/podushe/app/ui/screens/auth/PhoneNodeScreen.kt179R.drawable/R.string
main/java/ru/podushe/app/ui/screens/auth/PhoneScreen.kt136R.drawable/R.string
main/java/ru/podushe/app/ui/screens/chats/ChatFeed.kt75java.time
main/java/ru/podushe/app/ui/screens/chats/ChatState.kt92R.drawable/R.string
main/java/ru/podushe/app/ui/screens/chats/ChatsListNodeScreen.kt298R.drawable/R.string
main/java/ru/podushe/app/ui/screens/chats/ChatsListState.kt59R.drawable/R.string
main/java/ru/podushe/app/ui/screens/chats/OtherProfileState.kt78R.drawable/R.string
main/java/ru/podushe/app/ui/screens/chats/SubscriptionsNodeScreen.kt131R.drawable/R.string
main/java/ru/podushe/app/ui/screens/common/CardMapping.kt32R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/CreateEventNodeScreen.kt1090R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/CreateEventState.kt412R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/CreateSheets.kt177R.drawable/R.string, java.time, Locale, String.format
main/java/ru/podushe/app/ui/screens/create/EventQrNodeScreen.kt155R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/InviteNodeScreen.kt521R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/InviteState.kt106R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/ManageEventNodeScreen.kt252R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/ManageEventState.kt69R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/OnboardingArtLive.kt148R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/OrganizersNodeScreen.kt570R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/OrganizersState.kt141R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/ParticipantsNodeScreen.kt543R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/ParticipantsState.kt108R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/PlaceSheetNodeScreen.kt528R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/QuestionsNodeScreen.kt808R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/QuestionsState.kt186R.drawable/R.string
main/java/ru/podushe/app/ui/screens/create/UserProfileNodeScreen.kt648R.drawable/R.string
main/java/ru/podushe/app/ui/screens/favorites/FavoritesNodeScreen.kt332R.drawable/R.string
main/java/ru/podushe/app/ui/screens/favorites/FavoritesState.kt77R.drawable/R.string
main/java/ru/podushe/app/ui/screens/legal/PolicyNodeScreen.kt72R.drawable/R.string
main/java/ru/podushe/app/ui/screens/notifications/NotificationsNodeScreen.kt275R.drawable/R.string
main/java/ru/podushe/app/ui/screens/notifications/NotificationsState.kt92R.drawable/R.string, java.time, Locale
main/java/ru/podushe/app/ui/screens/profile/DiscountsState.kt45R.drawable/R.string
main/java/ru/podushe/app/ui/screens/profile/ProfileDiscountsNodeScreen.kt257R.drawable/R.string
main/java/ru/podushe/app/ui/screens/profile/ProfileEditNodeScreen.kt183R.drawable/R.string
main/java/ru/podushe/app/ui/screens/profile/ProfileEventsNodeScreen.kt188R.drawable/R.string
main/java/ru/podushe/app/ui/screens/profile/ProfileEventsState.kt77R.drawable/R.string, java.time, Locale
main/java/ru/podushe/app/ui/screens/profile/ProfileState.kt198R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/AboutForm.kt79java.time, String.format
main/java/ru/podushe/app/ui/screens/registration/AboutNodeScreen.kt343R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/AboutStepScreen.kt179R.drawable/R.string, java.time, Locale, BackHandler(activity)
main/java/ru/podushe/app/ui/screens/registration/CitySearchScreen.kt51R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/DistrictSearchScreen.kt59R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/InterestsNodeScreen.kt117R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/LocationStepScreen.kt84R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/SearchNodeScreen.kt179R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/VerifyNodeScreen.kt145R.drawable/R.string
main/java/ru/podushe/app/ui/screens/registration/VerifyStepScreen.kt38R.drawable/R.string
main/java/ru/podushe/app/ui/screens/settings/BlockedNodeScreen.kt173R.drawable/R.string
main/java/ru/podushe/app/ui/screens/settings/BlockedState.kt61R.drawable/R.string
main/java/ru/podushe/app/ui/screens/settings/SettingsNodeScreen.kt281R.drawable/R.string
main/java/ru/podushe/app/ui/screens/settings/SettingsState.kt81R.drawable/R.string
main/java/ru/podushe/app/ui/screens/swipes/SwipeNodeScreen.kt726R.drawable/R.string
main/java/ru/podushe/app/ui/screens/swipes/SwipeState.kt62R.drawable/R.string

Файлы класса О (переезжают без правок)

Файлы класса Д (кадры съёмки, debug)

Файлы класса Т (тесты)