Привет, Хабр! Меня зовут Александр Наумов, я руковожу разработкой мобильной платформы в VK Tech и Mail. В VK занимаюсь мобилками уже более десяти лет, и в этой статье я поделюсь с вами нашей внутренней кухней: как мы ищем инженерные решения, какого класса задачи мы решаем. Хочу поделиться нашей новой разработкой, которая, как мне кажется, может быть полезна сообществу.

Когда скорость и офлайн — не прихоть, а необходимость

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

В VK Tech мы ежедневно видим, как миллионы людей полагаются на наши сервисы (Облако, Почту, Android/iOS-приложения) в ситуациях, где задержка или недоступность — это не просто неудобство, а серьёзная проблема. Это накладывает уникальные требования: фичи должны быть быстрыми в разработке и доставке, но при этом — надёжно работать офлайн.

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

BDUI? Исследовали, отвергли. Жестко.

В поисках решения для динамических обновлений мы глубоко изучили подходы на базе Backend-Driven UI (BDUI). Увы, они категорически не подошли под наши реалии.

Самая принципиальная проблема — классический BDUI абсолютно зависим от бэкенда в режиме реального времени для получения вёрстки и данных. Нет сети — нет интерфейса, нет функционала. Для Почты и Облака, где работа офлайн — базовое требование, а не опция, это неприемлемо. Пользователь должен найти своё письмо или документ в любой ситуации.

Дополнительная нагрузка на команду и бэкенд. Чтобы понять, откуда она возникает, сравним сетевые взаимодействия классического клиента с BDUI-клиентом.

Для сравнения возьмём типичный кейс из мира приложений, где BDUI действительно распространён: просмотр корзины интернет-магазина и её очистка пользователем из UI-приложения. Схематично взаимодействие представлено ниже:

Видим классическое: клиент опрашивает API и получает профильные данные — счётчики заказов, размер корзины, аватары, имя/фамилия, etc. И, собственно, — список товаров в корзине. При её очистке посылается запрос на очистку, корзина предиктивно очищается в кеше клиента (помните же, что мы offline-first), и параллельно состояние корзины синхронизируется на эталонное — серверное.

Сравним с BDUI. Для примера примем ряд архитектурных допущений:

  • Клиент получает полностью «запечённое» состояние в виде слепка вёрстки;

  • Клиент не знает, какие бизнес-сущности он отображает;

  • Шаблонизация состояния в вёрстку выполняется отдельным сервисом Backend-for-Frontend (BFF);

  • BFF — stateless-сервис.

Блоки работы которых не было в классической схеме тут выделены голубым цветом. Рассмотрим их.

Построение контекста на BFF

Здесь выполняется вся работа, необходимая для того, чтобы собрать данные для шаблонизации, такие как: для какого пользователя мы строим вёрстку, какие именно товары мы будем вставлять в вёрстку и тому подобное.

Например, если мы где-то в вёрстке имеем текстовку вида: «Александр, у вас в корзине 6 товаров», то мы должны:

  • знать, как пользователя зовут;

  • знать, на каком языке ему написать сообщение, чтобы правильно просклонять числительное и выбрать корректный шаблон строки;

  • знать, сколько товаров.

BFF опрашивает нижележащие слои API для сбора этой информации.

Ресурсы, которые мы тратим здесь:

  • Рантайм-ресурсы: время пользователя и оперативная память на машинах, обслуживающих BFF;

  • Человеческие ресурсы: кто-то должен поддерживать код BFF при каждом изменении API.

Шаблонизация на BFF

Здесь полученные данные используются для того, чтобы сформировать вёрстку, которую мы отправим пользователю. Эффективно это означает, что писать вёрстку теперь будут бэкендеры либо люди, специально выделенные под эту работу.

Примем, что шаблоны вёрстки загружены в память и процесс заполнения шаблона не занимает много времени. Однако если мы что-то вёрстаем, то теперь делаем это здесь, а не в мобильном приложении.

Отправка верстки клиенту

По сравнению с ответами API, ответ BFF куда более многословен. API может отдать JSON-список товаров, BFF ещё должен рассказать, как каждый товар выглядит. Это нагрузка на сеть — как на клиентскую (деньги пользователя, потраченные на трафик; джоули батареи телефона, сгоревшие в радио; секунды времени на ожидание), так и на серверную сеть (трафик ДЦ не бесплатный).

Statelessness BFF-а приводит к дополнительной нагрузке на API нижнего уровня, что мы видим в нашем примере как дополнительный запрос профиля при очистке корзины.

Кроме того, мы видим проблемы с предиктивным изменением состояния клиента и, в целом, общую потребность в информировании клиента большим количеством данных, чем просто вёрстка. В современных продвинутых реализациях BDUI, таких как Flex, это решается тем, что помимо вёрстки в ответ добавляется ещё и контекстное дерево — чистое представление данных. При этом вёрстка не «запечена» полностью: часть данных клиент вставляет сам из контекстного дерева.

Также (характерно для Flex и некоторых других реализаций), для предиктивного изменения состояния вместе с действиями во вёрстке присылается микроскрипт, который интерпретируется клиентом и может выполнять простые операции, например: «очистить объект состояния по селектору пользователь/корзина». Это улучшает пользовательский опыт, но, опять же, очень сильно раздувает ответ BFF.

 Итого:

  • Сеть нужна постоянно;

  • Нагрузка вырастет;

  • При этом отзывчивость клиента скорее всего ухудшится;

  • Бонус: команда начнет страдать.

Нам это не подходит, поэтому нужно принципиально иное решение.

Динамика: возможность обновлять функциональность «по воздуху», минуя магазины.

Offline-first: фичи должны работать без доступа к сети, как вшитый код.

Эффективность: минимальные накладные расходы на инфраструктуру, никаких тяжёлых бэкендных генераций UI на каждый запрос.

Фокус на мобильных разработчиках. Решение должно быть:

  • максимально дружелюбным к нашим основным мобильным командам;

  • без необходимости привлекать армию JS-/бэкенд-спецов или переучиваться.

И мы его создали: Kotlett

Kotlett — это наша внутренняя платформа, которая реализует паттерн code-push, то есть динамическую доставку фич для Android- и iOS-приложений без использования BDUI и релиза приложения в магазине.

Его суть:

  • вы пишете фичу один раз на Kotlin, используя Compose-like DSL и классы фреймворка;

  • фича транспилируется в JS;

  • JS доставляется «по воздуху» как компактный бандл сразу в приложения под iOS и Android;

  • исполняется в защищённой среде на устройстве;

  • бесшовно интегрируется в нативное приложение через точки монтирования, работая полностью офлайн после загрузки;

  • при этом не нужен промежуточный бэкенд для генерации UI, а код пишут те же мобильные разработчики, которые годами делали то же самое для native.

В этой статье мы подробно расскажем, как устроен Kotlett, как он решает проблемы BDUI, обеспечивает офлайн-работу и почему мы считаем его перспективным фреймворком «динамики для народа». И главное — хотим понять, насколько такое решение востребовано сообществом.

Если интерес будет высоким, Kotlett имеет все шансы стать open source — всё зависит от вашей активности.

Kotlett под капотом: архитектура фреймворка

Kotlett родился как принципиально иная архитектура, отвергающая централизованную генерацию UI BDUI в пользу исполнения логики и рендеринга прямо на устройстве. Вот как это работает, кирпичик за кирпичиком:

Общая схема разработки. От разработчика до пользователя

Фича пишется на Kotlin Multiplatform (KMP) в едином модуле. Используются:

  • Классы компонентов жизненного цикла, предоставляемые фреймворком;

  • Compose-like DSL: для описания UI;

  • API Моста: для доступа к сервисам (сеть, навигация, хранилище).

  • Бизнес-логика: чистый Kotlin.

Сборка

Структурно Kotlett — KMP проект с четырмя модулями верхнего уровня: Common, JS, Android, IOS.

Common содержит общие классы и описывает интерфейсы, через которые будут взаимодействовать JS-компоненты с native.

JS-таргет содержит Kotlin-код, который будет преобразован в бандл — например, DSL для формирования UI и компоненты жизненного цикла фичи. Модуль компилируется стандартным Kotlin/JS-компилятором в оптимизированный (минифицированный, tree-shaken) JavaScript-бандл. Этот бандл содержит всю логику и UI-описание фич для работы офлайн. К бандлу прикладывается манифест — описатель того, какие API в хостовом приложении будет использовать бандл.

Android- и iOS-таргеты содержат платформоспецифичную реализацию фреймворка, которая принимает бандл и передаёт ему управление, то есть реализует рантайм. Здесь же лежит код, который позволяет легко встраивать и инициализировать фреймворк в хостовых приложениях. Эти модули компилируются в библиотеки, которые встраиваются в приложения, где мы используем фреймворк.

Вот пример кода инициализации фреймворка, который реализуют эти модули:

class DemoApplication : Application(), KotlettApplication {
 
    override lateinit var kotlett: Deferred<Kotlett>
 
    override fun onCreate() {
        super.onCreate()
        kotlett = CoroutineScope(Dispatchers.Default).async {
            createKotlett()
        }
    }
 
    @OptIn(ExperimentalCoroutinesApi::class)
    private suspend fun createKotlett(): Kotlett {
        return createKotlett {
            runtime { scope ->
                PlatformLogServiceFactory(this, scope) {
                    KotlettLogService
                }
                ...
            }
            appContext { this@DemoApplication }
            typefaceProviders {
                DemoTypefaceProviders(this@DemoApplication)
            }
            bundleSources(
                BundleSourceConfig.Remote(
                    url = { "http://x.y.z.j:8000/kotlett.bundle" },
                    parameters = RemoteBundleSource.Parameters(polling = true, pollingInterval = 30.seconds)
                ),
                BundleSourceConfig.Cached,
                BundleSourceConfig.Embedded
            )
            setDebugMode { BuildConfig.DEBUG }
            lookup { point -> point }
        }
    }
}

Доставка

Бандл с полученным исполняемым кодом р��змещается на CDN VK (это простой S3). Ссылка на актуальный бандл приходит в приложение через Remote Config (Firebase, AppConfig, etc.). Никакого бэкенда.

Приложение (с интегрированным Kotlett SDK) получает ссылку, загружает бандл по HTTPS. Верифицирует его, сравнивая встроенный в бандл манифест (список требуемых сервисов/методов) с манифестом, вшитым в версию Kotlett SDK. Несовместимый бандл отвергается, совместимый — помещается в кэш. В манифест с помощью KSP‑кодогенерации мы помещаем все сигнатуры всех методов клиента которые используются бандлом. Зная их — клиент может легко проверить что реализует все необходимое и отбросить несовместимый бандл:

Во время загрузки приложения фреймворк выбирает наиболее свежий бандл из имеющихся: может быть использован встроенный при сборке приложения, закешированный или форсирован удалённый (что полезно при отладке). Конкретная политика выбора бандла задаётся фреймворку клиентским приложением при интеграции:

private fun createBundlePollingSources(): List<BundleSourceConfig> = listOf(
        BundleSourceConfig.Remote(
            url = { dependencies().remoteParamsAccessorProvider().bundleUrl },
            parameters = RemoteBundleSource.Parameters(
                polling = true,
                pollingInterval = dependencies().remoteParamsAccessorProvider().bundlePollingPeriod.seconds,
            )
        ),
        BundleSourceConfig.Cached,
        BundleSourceConfig.Embedded,
    )

Ключевые сущности

Kotlett построен вокруг парадигмы «одна фича — один модуль». Фичи не знают друг о друге и живут параллельно в любом количестве инстансов. 

Чтобы создать фичу, разработчик в специальном (отдельном) репозитории с фичами определяет новый подмодуль с названием фичи. Писать с нуля не надо, мы поставляем шаблоны для фичей которые могут быть развернуты с использованием плагина GFT для IDEA. 

Все что нужно, чтобы добавить фичу — создать реализацию интерфейса FeatureModuleInstance и перегрузить единственный метод mount.

@Feature("kotlettIntegration")
class KotlettIntegrationModule(private val session: Session) : FeatureModuleInstance {
 
    internal val resources = session.resources()
 
    private val viewModel = session.obtain(KotlettIntegrationViewModel::class) {
        KotlettIntegrationViewModel(session)
    }
 
    override fun mount(): MountIntent {
        return MountIntent.DivKit(renderUi())
    }
 
    private fun renderUi() = mountDivKit(session) {
        card("kotlett_demo_feature") {
            state(0) {
                content()
            }
        }
    }
 
    private fun DivBuilder.content() {
        gallery(
            orientation = Gallery.Orientation.VERTICAL,
            modifier = Modifier
                .id("rootGallery")
                .fillMaxSize()
                .solidBackground(resources.colors.background)
                .paddings(session.context.windowInsets.asInsets())
        ) {
            closeButton()
            inputSection()
            pagerSection(viewModel.state.value)
            serviceSection()
        }
    }
 
...

Из приведенного примера вы во-первых можете увидеть большое сходство нашего DSL с Jetpack Compose и Swift UI, а во-вторых...

Да, мы используем DivKit как транспорт UI верстки из JS в native

Действительно, DivKit изначально создавался как библиотека, которая реализует кросс-платформенную сериализацию и рендеринг вёрстки, и это заметно упростило нашу жизнь. Ровно как и то, что DivKit поставляется по лицензии Apache 2.0, позволяющей использовать его в коммерческих продуктах.

Долгое время до внедрения Kotlett мы использовали DivKit в его «сыром» виде: привозили на клиент сырые кусочки вёрстки для мест, где критично иметь быструю сменяемость контента. Это не давало дополнительной мотивации разработчикам, вынужденным работать с километровым JSON.

Для Kotlett мы завернули DivKit в простой и понятный DSL, который взял на себя все проблемные места DivKit: формирование темплейтов, code completion, обработку коллбэков, нажатий и пр. Так, например, чтобы повесить click-коллбэк на элемент в нашем DSL, всего-то и нужно — применить модификатор clickable.

private fun DivBuilder.closeButton() {
        image(
            imageUrl = resources.images.close,
            modifier = Modifier
                .clickable("click 'Close'") {
                    viewModel.exitScreen()
                }
        )
    }

Под капотом DSL делает следующее:

  • при генерации JSON выделяет ID этому коллбэку, например 42;

  • в JSON-объекте image проставляет атрибут actions с URL вида kotlett://click-action?id=42;

  • в JS-части фреймворка сохраняет ссылку на замыкание внутри clickable() { } по ID 42;

  • в native-части фреймворка добавляет перехватчик на kotlett://click-action?id=42 и при его срабатывании передаёт управление JS-части по ID 42.

Всё это значительно упрощает работу программиста и полностью снимает с нас необходимость поддерживать кросс-платформенный рендер вёрстки.

Запуск фичи

Всё начинается с того, что разработчик размещает в вёрстке приложения специальную View – точку монтирования. Это область, куда будет рендериться контент фичи. Точке монтирования атрибутируется название фичи, которая в ней будет жить, и параметры, которые будут переданы фиче на старте.

Далее нативная часть фреймворка перехватывает создание точки монтирования и запускает инициализацию фреймворка. Это делается один раз на всё приложение, независимо от того, сколько фич отображается:

  • запускается JS‑движок — на Android это J2V8, на iOS — JavaScriptCore;

  • выполняется базовый скрипт JS‑рантайма: он наполняет пространство объектов внутри движка «ручками», через которые будет вестись взаимодействие.

    Инициализация фичи: движок находит точку входа JS‑кода фичи, создаёт изолированную сессию и передаёт ей управление. Фича теперь работает, но её ещё не видно. Вот пример инициализации активити, где весь контент экрана — точка монтирования фичи Kotlett:

@AndroidEntryPoint
class AutouploadKotlettSplashActivity : KotlettActivity() {
 
    @Inject
    lateinit var remoteParams: RemoteParamsProvider
 
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
    }
 
    override fun initContentView() {
        val mountPoint = MountPointView(this).apply {
            layoutParams = FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT
            )
        }
        setContentView(mountPoint)
 
        val isFromSettings = intent?.getBooleanExtra(IS_FROM_SETTINGS_PARAM, false) ?: false
        val splashVariant = if (isFromSettings) {
            remoteParams().disableAutouploadKotlettSplashVariant
        } else {
            remoteParams().autouploadKotlettSplashVariant
        }
 
        mountPoint.setMountPointProperties(
            name = AUTOUPLOAD_KOTLETT_SPLASH_FEATURE_NAME,
            applySystemInsets = true,
            params = mapOf(
                "splash_variant" to splashVariant.toString(),
                "is_from_settings" to isFromSettings.toString()
            )
        )
    }
 
...

Исполнение

Всё, что требуется от фичи, чтобы начать взаимодействие с пользователем, — это вернуть определённый объект, содержащий вёрстку, из метода mount(), который будет вызван фреймворком автоматически (близкий аналог — onCreateView из Fragment).

Обновления состояния

Из mount() фича может возвращать не только команду показать DivKit, но и несколько других. Например:

  • MountIntent.MountPoint — приём такого интента заставит точку монтирования переключить фичу в себе на ту, которая указана в Intent-е.

Если вдруг фича понимает, что дерево UI нужно перестроить, она просто вызывает remount(), что запускает цикл генерации DivKit JSON заново.

Для более мелких (точечных) обновлений UI не обязательно перестраивать всё дерево — DivKit умеет привозить изменения в поля точечно, и мы это поддержали в нашем DSL.

  .. в mount()
       text(
            textFlow = model.descriptionCounterTxt, // тут передаем не String а Flow<String>
            textColor = Resources.Colors.textSecondary,
            fontSize = Resources.Values.footnoteFontSize,
            modifier = Modifier
                .margins(bottom = 25)
        )
 
 
 
    .. во ViewModel
        private fun update(newState: State, remount: Boolean = false) {
            state = newState
            ...
 
            descriptionCounterTxt.value = "${state.description.length} / $maxDescriptionSize"
        }

Здесь, например, видно, что ViewModel может предоставить для UI-дерева StateFlow, который можно напрямую передать в DSL, и он сам начнёт следить за её изменениями и передавать значения в дерево DivKit без его перестроения.

Не-UI взаимодействия

Фичи призваны не только показывать пользователю что-то красивое, но и делать полезную работу. Для этой цели фича может обратиться к одному из core-сервисов, например, чтобы:

  • сделать сетевой вызов (авторизация будет предоставлена хостовым приложением);

  • сохранить данные в KV-хранилище;

  • прочитать/сохранить что-то с диска через файловый сервис;

  • прочитать какой-нибудь remote config флаг;

  • выполнить навигационное действие — перейти по диплинку или, например, вызвать goBack().

Все эти сервисы реализованы с поддержкой Kotlin корутин на базе открытых KMP проектов, таких как Ktor и FileKit.

Например, так фича может инициировать сетевой запрос с помощью NetworkService:

fun sendReport() {
        if (!isValid(state)) {
            update(state.copy(uploadingState = State.Uploading.UPLOAD_VALIDATION_ERROR), true)
            return
        }
 
        session.scope.launch {
            update(state.copy(uploadingState = State.Uploading.UPLOADING), true)
            val response = networkService.request(buildReportRequest())
            if (response?.code == 200) {
                update(state.copy(uploadingState = State.Uploading.UPLOAD_SUCCESS), true)
                navigationService.goBack()
            } else {
                update(state.copy(uploadingState = State.Uploading.UPLOAD_ERR), true)
            }
        }.let {
            update(state.copy(uploadJob = it))
            it.invokeOnCompletion {
                update(state.copy(uploadJob = null, uploadingState = State.Uploading.IDLE), true)
            }
        }
    }
 
    private fun buildReportRequest() = buildNetworkRequest {
        method(HttpMethod.POST)
        url("${requestUrl}?client_id=${PLACEHOLDER_OAUTH_CLIENT_ID}")
        headers {
            set(HttpHeaders.Authorization, "Bearer $PLACEHOLDER_ACCESS_TOKEN")
        }
        setMultiPartFormDataBody {
            formData {
                append("\"description\"", state.description)
                state.files.forEach { file ->
                    appendFile("\"files\"", file.uri) {
                        set(HttpHeaders.ContentType, "text/plain")
                        set(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"")
                    }
                }
            }
        }
    }

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

Год после первого релиза: реальный опыт эксплуатации и цифры

Наш фреймворк живёт уже около года в Почте и Облаке под iOS и Android. Сейчас на нём написано уже 42 фичи — от маленьких баннеров до полноэкранных списков тарифов.

Выпущено более 70 релизов бандла, что соразмерно с годовым количеством релизов приложений.

Стало быстрее?

Для нашей разработки мы измеряем показатели доставки по индустриальному стандарту DORA. При нашем недельном релизном цикле для обычных фич, выпущенных по-старому, мы видим медианное время «от мержа до пользователя» около 40 рабочих часов. Для Kotlett-фич этот же показатель составляет несколько часов и иногда — до 2 дней. Продуктовые команды получают медианное ускорение в 25%, используя Kotlett для экспериментов и быстрых правок в смеси с классической нативной разработкой.

Вызовы с которыми мы столкнулись

JS-движок добавляет к APK около 30 МБ, это много, но в современных реалиях не критично. Для iOS этой проблемы нет: JSCore — это большое преимущество. Есть небольшой шанс, что когда-нибудь транспиляция Kotlin под WebAssembly будет так же хорошо поддержана, и мы сможем убрать тяжёлый V8 и заменить его на лёгкий движок WebAssembly.

Количество времени, которое разработчики тратят на отладку и тестирование, оказалось, вероятно, самой большой проблемой, поэтому мы:

  • сделали «тестовую фичу», которая, разворачиваясь в каждом приложении, показывает правильность интеграции;

  • сделали режим hot-reload, когда достаточно в IDE пересобрать фичу, а приложение на телефоне за 1–5 секунд стянет свежий бандл и переразвернёт рантайм. Это помогает в случаях дизайн-ревью или когда фича глубоко закопана в приложении;

  • вывели логи и перехваченные креши в хостовые приложения, чтобы при отладке инцидентов мы видели, что происходит внутри.

А вот переход на кроссплатформенную разработку на KMP не оказался проблемой даже для iOS-разработчиков. Kotlin очень похож на Swift, а наш DSL для DivKit — на SwiftUI. Один из промоутеров фреймворка в нашем внутреннем комьюнити — iOS-разработчик, и мы гордимся этим.

Будущее и вопрос сообществу

С помощью этой статьи мы хотим узнать, нужно ли открыть код фреймворка Kotlett, сделав его народным? Хотели бы увидеть его в open source?

  • Решает ли он ваши боли?

  • Какие функции или улучшения были бы для вас критичны, чтобы принять решение о его использовании?

Если вам интересно, как мы будем развивать фреймворк и какие расширения появятся в ближайшем будущем — задавайте вопросы в комментариях! Делитесь своими идеями и сценариями использования, мы обязательно ответим и с радостью обсудим ваши предложения. Нам важно понять ваш интерес и услышать обратную связь.

Комментарии (6)


  1. nbkgroup
    02.10.2025 11:12

    Давайте представим, что вы пользователь Облака Mail. Сеть пропала, а вам срочно нужны сканы паспорта из загруженных файлов, например, для подтверждения возраста

    Хранить сканы паспорта в публичном облаке - это безумие.


    1. levashove
      02.10.2025 11:12

      Мы знали, что вы про это напишите.)


    1. Newbilius
      02.10.2025 11:12

      О, кстати, давно хотел спросить: а что может злоумышленник сделать со сканом паспорта? Фото или его распечатка вроде бы не могут быть основанием ни для чего, для чего может быть использован реальный паспорт. Кредит по скану не оформят, счёт не откроют. Подтвердить или опровергнуть грузите ли вы свой паспорт или чужой онлайн-сервисы компетенции не имеют, а значит и считать его реальным документом - тоже не имеют права.


  1. Newbilius
    02.10.2025 11:12

    нужно ли открыть код фреймворка Kotlett, сделав его народным? Хотели бы увидеть его в open source? Решает ли он ваши боли?

    А чтоб это понять, действительно ли он решает боли или добавит новых - нужно попробовать на нём пописать ;-)


    1. GolovinDS
      02.10.2025 11:12

      Звучит резонно. Но получается, что интерес попробовать есть, верно?)


      1. Newbilius
        02.10.2025 11:12

        Определённо) BDUI всегда казался очень полезной в определённых ситуациях, но всё же тяжеловесной штукой. Тут подход интереснее...

        Хотел ещё уточнить: вроде ходила "всем известная" (но я ни разу её не проверял) информация, что Apple не разрешает сторонние интерпретаторы кода тащить с собой в приложение. Я так понимаю, сейчас такого ограничения уже нет?