Сначала коротко о том, зачем нам это было нужно.

Мы в основном пилим решения для фудтеха, а для мобилок используем React Native (почему, рассказывали тут). 

В одном из таких проектов (российская сеть ресторанов по франшизе) нам нужно было прикрутить Яндекс Карты. Изначально хотели взять либу react-native-yamap (респект тем, кто ее делал) — но как выяснилось, она работает только на старой архитектуре RN. 

После обновления до 0.76 версии, где Fabric стала использоваться по умолчанию, приложения на iOS начали падать: карта не рендерится, события не доходят до JS, приложение крашится при взаимодействии с картой и вот это вот всё. И судя по открытым тикетам, мы не одни, кто столкнулся с этой проблемой. 

Полезли искать, написал ли кто-то уже библиотеку под новую архитектуру — но либо таких людей нет, либо ни с кем не делятся. Спойлер: мы пока тоже не будем, ещё обкатываем либу на своих проектах — но уже сейчас хотим рассказать, как собрали новый пакет с помощью Claude Code за два дня.

Форкать или не форкать: вот в чем вопрос

Изначально мы хотели взять эт�� либу, форкнуть, указать родительский репозиторий и с помощью ИИ-шки самим всё обновить. Чтобы понять, почему библиотека сломалась, начали разбираться, что изменилось в архитектуре React Native.

Старая архитектура работала через Bridge. JS-код сериализовал данные в JSON, отправлял через асинхронную шину в нативный слой, там происходила десериализация, выполнялся нативный код и ответ прилетал обратно тем же путём. 

Новая работает через JSI. Он вызывает нативный код напрямую через C++ и получает результат без сериализации — как вы понимаете, «это другое».

Или два фундаментально разных подхода к тому, как JS разговаривает с нативом:

Paper: JS → JSON → Bridge → ObjC/Java → YMKMapView  
Fabric: JS → JSI → C++ → ObjC/Kotlin → YMKMapView

Какие из-за этого вылезли проблемы? Вот основные:

  1. ViewManager’ы написаны под Paper. Для обновления нужны ViewComponentView для iOS и ViewManagerDelegate для Kotlin — а это уже больше переписывание, чем рефакторинг. 

  2. Нет Codegen-спецификаций, которые Fabric требует в обязательном порядке. 

  3. Callback’и через Bridge. Все асинхронные вызовы построены на Bridge-паттернах, а новая архитектура требует TurboModules с другой моделью взаимодействия. 

  4. События через sendEvent. В Fabric они работают через DirectEventHandler’ы с конкретным именованием — top + PascalCase. Это не критично само по себе, но вкупе означает, что придётся трогать буквально каждый файл. 

Мы поняли, что если пытаться всё это добавить поверх старого кода, получится «франкенштейн»: обёртки над обёртками, костыли вместо архитектуры, ещё и риски, что что-то всё равно отвалится при следующем обновлении SDK. Плюс, влить столько изменений без поломки обратной совместимости практически нереально, а старые пользователи все еще сидят на Paper.

Поэтому мы решили переписать библиотеку с нуля: с последним SDK (за пару недель до Яндекс выпустил 4.29), поддержкой его компонентов (даже можно строить свои маршруты) и новой архитектуры (Fabric, TurboModule, полная типизация через Codegen).

Что из этого получилось

Архитектура новой библиотеки выглядит так:

src/
├── specs/           # Codegen спецификации (источник истины)
│   ├── NativeYaMap.ts
│   ├── YaMapViewNativeComponent.ts
│   └── ...
├── components/      # React-обёртки
│   ├── YaMap.tsx
│   ├── Marker.tsx
│   └── ...
└── modules/         # JS-модули (Suggest, Search, Geocoder)

ios/
├── Modules/         # TurboModules
└── Components/      # Fabric ViewComponentView

android/
├── modules/         # TurboModules  
└── views/           # ViewManager + ViewManagerDelegate

Одна из проблем Fabric: Commands не поддерживают Promise-callback'и напрямую. Нельзя вызвать нативный метод и передать туда функцию, которая вызовется с результатом. Это фундаментальное ограничение модели. 

Решение — паттерн с уникальными идентификаторами:

// JS: генерируем уникальный ID
const callbackId = generateId();
pendingCallbacks.set(callbackId, resolve);
Commands.getCameraPosition(ref, callbackId);

// Native: отправляем событие с ID
onCameraPositionReceived({ id, zoom, tilt, ... })

// JS: резолвим Promise по ID
const callback = pendingCallbacks.get(event.id);
callback(event.data);

Ещё один важный момент — композиция вместо наследования при работе с YMKMapView. Вместо того чтобы наследоваться от нативного вью напрямую, мы оборачиваем его внутрь UIView. Это даёт больше контроля над жизненным циклом и упрощает интеграцию с Fabric:

class YaMapViewComponentView: UIView {
    private let mapView = YMKMapView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(mapView)
    }
}

Благодаря Codegen типобезопасность уже становится ответственностью компилятора — не вашей. Он генерирует интерфейсы из TypeScript-спецификаций, и нативный код обязан им соответствовать. Не реализовал метод из спеки — не скомпилируется:

class YaMapMarkerViewManager : 
    YaMapMarkerManagerInterface<YaMapMarkerView> {
    
    // Компилятор требует реализовать ВСЕ методы из спеки
    override fun setLat(view: YaMapMarkerView, value: Double)
    override fun setLon(view: YaMapMarkerView, value: Double)
    // ...
}

Два дня с Claude vs. две недели без него

Нативная разработка — не наш основной стек. Мы пишем на TypeScript, хорошо знаем React Native изнутри, представляем, как устроены нативные модули в теории — но Swift и Kotlin понимаем на уровне «разобраться, если очень надо». 

Если бы мы стали делать либу без ИИ (и были оптимистами), это заняло бы у нас минимум две недели. Потому что пришлось бы самим копаться в документации Яндекс SDK, вручную писать обвязку для каждого нативного модуля, а каждую свифтовую ошибку, которая валится в рантайме, по часу искать и отлаживать. 

Поэтому мы решили перекинуть эту работу на нашего «лучшего джуна». Или как сформулировал он сам:

Пойдём по порядку.

  1. Начали с анализа оригинальной библиотеки. Загрузили код, попросили Claude разобраться в структуре, выделить все публичные API, понять, что именно делает каждый компонент и как он взаимодействует с Яндекс SDK. Это дало понимание, что нужно реализовать.

  2. Прежде чем писать нативный код, спроектировали Codegen-спецификации — файлы TypeScript, которые описывают интерфейс компонентов и модулей. Claude помогал продумать типизацию, находил случаи, где типы на iOS и Android расходятся, и предлагал решения. 

  3. Собственно, генерация нативного кода для каждого компонента: сначала спека, потом iOS-реализация, затем Android. Claude писал код, объяснял решения и указывал на подводные камни Fabric. 

Как этот код выглядит? Обратите внимание не только на синтаксис, но и на то, где именно происходит проверка типов. 

Старый ViewManager на Java:

public class YamapViewManager extends ViewGroupManager<YamapView> {
    @ReactProp(name = "center")
    public void setCenter(YamapView view, ReadableMap center) {
        if (center != null) {
            double lat = center.getDouble("lat");
            double lon = center.getDouble("lon");
            // ... парсинг, проверки, обработка ошибок
        }
    }
}

Новый ViewManager на Kotlin с Codegen:

@ReactModule(name = NAME)
class YaMapViewManager : SimpleViewManager<YaMapView>(),
    YaMapViewManagerInterface<YaMapView> {  // ← Codegen интерфейс
    
    override fun setInitialRegion(view: YaMapView, value: ReadableMap?) {
        // Типы гарантированы Codegen
        view.setInitialRegion(value)
    }
}

4. Переходим к итерациям. Код компилируется, но что-то не работает на устройстве — допустим, из-за несоответствия имени события. Codegen требует, чтобы на Android они именовались строго как top+ PascalCase, а у нас написано markerPress вместо topMarkerPress — и событие просто не приходит в JS. Без ИИ-шки поиск проблемы занял бы у нас часы, а с ней — пять минут. 

Около четырёх часов диалога — и у нас есть рабочая библиотека. Откуда знаем, что рабочая?

Тестовое приложение

Юнит-тесты тут особо не помогут: нужно запускать на реальном устройстве, тыкать в карту руками и смотреть, что происходит. Проверять на реальном приложении и смотреть, не сломается ли что-нибудь уже в проде, тоже не самая классная идея.

Поэтому параллельно с библиотекой мы написали тестовое приложение как под Android, так и под iOS, которое покрывает все основные фичи и компоненты, которые используются в Яндекс Картах: Marker, Circle, Polygon, Polyline, кластеры. Пишем функцию — и сразу проверяем на живом устройстве.

Как бонус, теперь это приложение работает как страховка на будущее. Когда Яндекс выпустит новый SDK (а это случается регулярно), мы просто обновим библиотеку, запустим аппку и сразу увидим, что изменилось или сломалось. 

Что в итоге-то

Библиотека полностью поддерживает новую архитектуру React Native Fabric и TurboModules, строгую типизацию через Codegen и последний SDK от Яндекса. Кода стало меньше, функциональности больше, архитектура чище.

Из компонентов есть карта, маркеры, полигоны, полилинии, круги, кластеризация маркеров. Из функционала — поиск, геоподсказки, геокодирование, построение маршрутов.

Сам Claude суммировал так:

Всем, кто боится, что ИИ отнимет у нас работу, пора напрячься: нейронка уже даже оценивает сроки как реальный разработчик. По факту, конечно, две недели у нас бы ушло без Claude — с ним всё заняло два дня. 

Мораль

Если >50% библиотеки нужно переписывать, лучше начать с нуля. В этом случае старый код — уже не фундамент, а якорь: без него может получиться быстрее, проще и чище. Особенно с нейронкой, которая сэкономила нам минимум 8 рабочих дней.

Как и сказали в начале, либу для Яндекс Карт пока что в опенсорс выкладывать не будем. Но у нас есть другие:

  • react-native-device-country — определяет местоположение устройства по SIM-карте, без GPS (только на Android);

  • react-native-wallet-manager — даёт доступ к базовым функциям Apple Wallet: добавление и удаление пассивов, проверка карт и билетов на существование (подробнее рассказывали тут);

  • next-pwa-pack делает из любого Next.js-проекта полноценное PWA одной строкой (тоже делились статьёй на тему).

С вопросами, предложениями и «вот у нас такая же штука была» ждём в комментариях. Ну и подписывайтесь, конечно — если передумаем и решим делиться библиотекой, то напишем сюда.