20 апр. 2025 г.·7 мин

Сетевое взаимодействие на Kotlin при медленных соединениях: таймауты и безопасные повторы

Практические советы по сетевому взаимодействию на Kotlin при медленных соединениях: настраивайте таймауты, кэшируйте безопасно, повторяйте запросы без дубликатов и защищайте критичные действия в нестабильных мобильных сетях.

Сетевое взаимодействие на Kotlin при медленных соединениях: таймауты и безопасные повторы

Что ломается при медленных и нестабильных соединениях

На мобильных устройствах «медленно» обычно не значит «нет интернета». Чаще это соединение, которое работает только короткими всплесками. Запрос может занимать 8–20 секунд, зависнуть посередине, а потом завершиться. Либо он срабатывает в один момент и падает в следующий, потому что телефон переключился с Wi‑Fi на LTE, попал в зону слабого сигнала или ОС перевела приложение в фон.

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

Здесь дефолтные настройки дают сбой. Многие приложения полагаются на значения библиотек для таймаутов, повторов и кэширования, не решив заранее, что значит «достаточно хорошо» для реальных пользователей. Дефолты часто ориентированы на стабильный Wi‑Fi и быстрые API, а не на поездку в поезде, лифте или шумное кафе.

Пользователи не говорят «socket timeout» или «HTTP 503». Они замечают симптомы: бесконечные спиннеры, внезапные ошибки после долгого ожидания (а потом всё работает при повторе), дублирующиеся действия (два бронирования, два заказа, двойное списание), потерянные обновления и странные состояния, когда UI пишет «ошибка», а сервер уже успел выполнить запрос.

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

«Критические действия» — это всё, что должно произойти не более одного раза и быть корректным: платежи, подтверждение заказа, бронирование слота, перевод баллов, смена пароля, сохранение адреса доставки, подача заявки или отправка одобрения.

Реалистичный пример: кто‑то подтверждает заказ по слабому LTE. Приложение отправляет запрос, соединение падает до прихода ответа. Пользователь видит ошибку, нажимает «Оплатить» снова — и два запроса доходят до сервера. Без ясных правил приложение не понимает, следует ли повторять, ждать или остановиться. Пользователь не знает, стоит ли пробовать ещё.

Решите правила до правки кода

При медленных или нестабильных соединениях большинство багов возникает из‑за неясных правил, а не из‑за HTTP‑клиента. Прежде чем трогать таймауты, кэширование или повторы, зафиксируйте, что значит «правильно» для вашего приложения.

Начните с действий, которые никогда не должны выполняться дважды. Обычно это операции с деньгами и аккаунтом: оформление заказа, списание с карты, выплата, смена пароля, удаление аккаунта. Если пользователь нажал дважды или приложение повторяет запрос, сервер всё равно должен трактовать это как одну операцию. Если вы ещё не можете это гарантировать, пометьте такие эндпоинты как «нет авто‑повторов» до тех пор, пока не обеспечите безопасность.

Далее решите, что может делать каждый экран при плохой сети. Некоторые экраны полезны офлайн (последний профиль, предыдущие заказы). Другие должны стать только для чтения или показывать ясное состояние «попробуйте снова» (остатки на складе, актуальные цены). Смешивание этих ожиданий ведёт к запутанному UI и рискованному кэшированию.

Установите допустимое время ожидания для каждого действия на основе восприятия пользователя, а не «красоты» кода. Логин может терпеть короткую задержку. Загрузка файлов требует больше времени. Оформление заказа должно быть быстрым, но и безопасным. Таймаут в 30 секунд может быть «надёжным» на бумаге и при этом ощущаться сломанным.

Наконец, решите, что хранить на устройстве и как долго. Кэш полезен, но устаревшие данные могут привести к неправильным решениям (старые цены, просроченная акция).

Запишите правила в месте, где их легко найти (README подойдёт). Держите просто:

  • Какие эндпоинты «нельзя дублировать» и требуют обработки идемпотентности?
  • Какие экраны должны работать офлайн, а какие — только в режиме чтения при отсутствии сети?
  • Какое максимально допустимое время ожидания для действия (логин, обновление ленты, загрузка, чек-аут)?
  • Что можно кешировать на устройстве и какое время жизни у кеша?
  • После ошибки показываем сообщение, ставим в очередь для повторной отправки или требуем ручной retry?

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

Таймауты, соответствующие ожиданиям пользователей

Медленные сети ломаются по‑разному. Хорошая настройка таймаутов не просто «берёт число». Она соотносится с тем, что пытается сделать пользователь, и прерывает операцию достаточно быстро, чтобы приложение могло восстановиться.

Три таймаута, простыми словами:

  • Connect timeout: сколько ждать установления соединения с сервером (DNS, TCP, TLS). Если он падает, запрос фактически не начался.
  • Write timeout: сколько ждать при отправке тела запроса (загрузки, большой JSON, медленный канал отдачи).
  • Read timeout: сколько ждать, пока сервер начнёт присылать данные после отправки запроса. На ненадёжных мобильных сетях это встречается чаще всего.

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

Практическая отправная точка (настраивайте после измерений):

  • Загрузка списка (низкий риск): connect 5–10 с, read 20–30 с, write 10–15 с.
  • Поиск по мере ввода: connect 3–5 с, read 5–10 с, write 5–10 с.
  • Критичные действия (высокий риск, типа «Оплатить»): connect 5–10 с, read 30–60 с, write 15–30 с.

Согласованность важнее совершенства. Если пользователь нажал «Отправить» и видит спиннер две минуты, он нажмёт снова.

Избегайте «вечной загрузки», задав верхнюю границу и в UI. Показывайте прогресс сразу, позволяйте отменить, и через (скажем) 20–30 секунд показывайте «Все ещё пытаемся…» с опциями повторить или проверить соединение. Это честнее, даже если сетевая библиотека всё ещё ждёт.

Когда таймаут случается, логируйте достаточно данных для отладки паттернов, но не логируйте секреты. Полезны: путь URL (не полный query), HTTP‑метод, статус (если есть), разбиение времени (connect vs write vs read, если доступно), тип сети (Wi‑Fi, сотовая, режим полёта), примерный размер запроса/ответа и ID запроса, чтобы сопоставить клиентские и серверные логи.

Простой и единый сетевой набор правил на Kotlin

При медленных соединениях маленькие несоответствия в конфигурации клиента перерастают в большие проблемы. Чистая базовая настройка помогает быстрее отлаживать и даёт каждому запросу одинаковые правила.

Один клиент, одна политика

Начните с единого места, где строите HTTP‑клиент (обычно один OkHttpClient, используемый Retrofit). Поместите туда базовое: дефолтные заголовки (версия приложения, локаль, токен), User‑Agent, таймауты в одном месте (не раскидывайте по вызовам), логирование, которое можно включать для отладки, и одно решение по ретраям (хотя бы «нет автоматических повторов»).

Ниже небольшой пример, где конфигурация держится в одном файле:

val okHttp = OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(20, TimeUnit.SECONDS)
  .writeTimeout(20, TimeUnit.SECONDS)
  .callTimeout(30, TimeUnit.SECONDS)
  .addInterceptor { chain ->
    val request = chain.request().newBuilder()
      .header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
      .header("Accept", "application/json")
      .build()
    chain.proceed(request)
  }
  .build()

val retrofit = Retrofit.Builder()
  .baseUrl(BASE_URL)
  .client(okHttp)
  .addConverterFactory(MoshiConverterFactory.create())
  .build()

Центральная обработка ошибок, которая мапит в сообщения пользователю

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

Сделайте один маппер, который превращает неудачи в небольшое множество дружелюбных исходов: нет соединения/режим полёта, таймаут, ошибка сервера (5xx), ошибка валидации или авторизации (4xx) и неизвестная ошибка. Это поддерживает единообразие UI‑копии («Нет соединения» vs «Попробуйте снова») без утечки технических деталей.

Тегируйте и отменяйте запросы при закрытии экранов

На нестабильной сети вызовы могут завершиться поздно и обновить уже закрытый экран. Сделайте отмену стандартным правилом: при закрытии экрана работа прекращается.

С Retrofit и Kotlin корутинами отмена scope (например в ViewModel) отменяет связанный HTTP‑вызов. Для некорутиных вызовов храните ссылку на Call и вызывайте cancel(). Также можно тегировать запросы и отменять группы вызовов при выходе из фичи.

Фоновая работа не должна зависеть от UI

Всё важное, что должно завершиться (отправка отчёта, синхронизация очереди, завершение отправки), должно запускаться в планировщике задач. На Android обычно это WorkManager — он может повторять попытки позже и переживает рестарт приложения. Делайте UI лёгким и передавайте длительные задачи в фон, когда это имеет смысл.

Правила кэширования, безопасные для мобильных

Сделать checkout устойчивым
Прототипируйте безопасный платежный поток с бэкендом, дружелюбным к идемпотентности, и чистым UI клиента.
Собрать checkout

Кэширование даёт большой выигрыш при медленных сетях: уменьшает повторные загрузки и делает экраны мгновенными. Но оно же опасно, если показывает устаревшие данные в неправильное время — например старый баланс или неверный адрес доставки.

Безопасный подход — кэшировать только то, что пользователь может допустить немного устаревшим, и требовать свежести для всего, что влияет на деньги, безопасность или финальное решение.

Базовые Cache‑Control правила

Большинство решений завязано на нескольких заголовках:

  • max-age=60: можно повторно использовать кешированный ответ в течение 60 секунд без проверки на сервере.
  • no-store: не сохранять этот ответ вообще (лучше для токенов и чувствительных экранов).
  • must-revalidate: если истёк срок, нужно проверить на сервере перед повторным использованием.

На мобильных устройствах must-revalidate предотвращает «тихое» использование неверных данных после краткого офлайна. Если пользователь открыл приложение после поездки в метро, вы хотите быстрый экран, но и подтверждение актуальности данных.

ETag‑валидация: быстро, дешево и надёжно

Для чтения ETag‑валидация часто лучше длинного max-age. Сервер шлёт ETag с ответом. В следующий раз приложение отправляет If-None-Match с этим значением. Если ничего не изменилось, сервер возвращает 304 Not Modified — это маленький и быстрый ответ на слабой сети.

Это хорошо подходит для списков товаров, профилей и экранов настроек.

Правило простое:

  • Кешируйте «read» эндпоинты с коротким max-age и must-revalidate, и поддерживайте ETag где возможно.
  • Не кешируйте «write» эндпоинты (POST/PUT/PATCH/DELETE). Считайте их всегда сетевыми.
  • Для всего чувствительного используйте no-store.
  • Кеш статических ассетов (иконки, публичная конфигурация) можно держать дольше.

Держите решения по кэшированию согласованными по всему приложению — пользователи замечают несоответствия сильнее, чем небольшие задержки.

Безопасные повторы, которые не усугубляют ситуацию

Превратить правила в API
Проектируйте модели данных и API визуально, затем выпускать согласованное поведение на всех клиентах.
Создать API

Повторы кажутся лёгким решением, но они могут навредить. Повторяйте неправильные запросы — получите лишнюю нагрузку, разряд батареи и ощущение, что приложение зависло.

Начните с повторов только для ошибок, которые, вероятно, временные. Падение соединения, таймаут чтения или кратковременный сбой сервера могут пройти при следующей попытке. Неправильный пароль, отсутствующее поле или 404 — нет.

Практическое правило:

  • Повторять таймауты и ошибки соединения.
  • Повторять 502, 503 и иногда 504.
  • Не повторять 4xx (кроме 408 или 429, если у вас ясное правило ожидания).
  • Не повторять запросы, которые уже дошли до сервера и могут быть в обработке.
  • Держать число попыток низким (обычно 1–3).

Backoff + jitter: уменьшить волны повторов

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

Например: ждать ~0.5 с, затем 1 с, затем 2 с, с случайным отклонением ±20%.

Ограничьте общее время повторов

Без лимитов повторы могут держать пользователя в спиннере минуты. Выберите максимальное суммарное время для всей операции, включая ожидания. Многие приложения стремятся к 10–20 секундам, прежде чем показать ясный выбор пользователю.

Также соотнесите контекст: при отправке формы пользователь хочет быстрый ответ; фоновая синхронизация может повторяться позже.

Никогда не делайте автоматические повторы для неидемпотентных действий (например, оформление заказа или отправка платежа), если у вас нет защиты вроде idempotency key или серверной проверки дубликатов. Если безопасности нет — завершайте с ошибкой и дайте пользователю решить, что делать.

Предотвращение дубликатов для критичных действий

При медленном или нестабильном соединении пользователи нажимают второй раз. ОС может повторить в фоне. Ваше приложение может повторить по таймауту. Если действие «создаёт что‑то» (заказ, перевод денег, смена пароля), дубликаты опасны.

Идемпотентность означает, что один и тот же запрос должен давать тот же результат. При повторении сервер не должен создавать второй заказ; он должен вернуть оригинальный ответ или сообщить «уже выполнено».

Используйте idempotency key для каждой критичной попытки

Для критических действий генерируйте уникальный ключ идемпотентности при старте попытки и отправляйте его с запросом (обычно в заголовке Idempotency-Key или в теле).

Практический поток:

  • Создайте UUID idempotency key при нажатии «Оплатить».
  • Сохраните его локально с небольшой записью: status = pending, createdAt, хеш payload.
  • Отправьте запрос с ключом.
  • При успехе пометьте status = done и сохраните ID результата от сервера.
  • При повторе используйте тот же ключ, а не новый.

Именно правило «переиспользовать тот же ключ» останавливает случайные двойные списания.

Обрабатывайте перезапуски приложения и офлайн‑пробелы

Если приложение убито посередине запроса, при следующем запуске нужно быть в безопасности. Храните ключ и состояние запроса в локальном хранилище (например, в небольшой строке БД). При рестарте либо повторите с тем же ключом, либо вызовите endpoint «проверить статус», используя сохранённый ключ или ID результата.

Со стороны сервера контракт должен быть ясен: при повторном ключе он либо отклоняет вторую попытку, либо возвращает первоначальный ответ (тот же order ID, тот же чек). Если сервер не умеет так работать, клиентская защита никогда не будет полностью надёжной, потому что клиент не увидит, что случилось после отправки запроса.

Полезная фишка для пользователя: если попытка в состоянии pending, показывайте «Платёж выполняется» и блокируйте кнопку до окончательного результата.

UI‑паттерны, которые уменьшают случайные повторные отправки

Создавать надёжные внутренние инструменты
Создавайте внутренние инструменты и админ-панели, которые работают даже при ненадежной сети.
Посмотреть платформу

Медленные сети меняют поведение людей. Когда экран подвисает пару секунд, многие думают, что ничего не произошло, и нажимают снова. Ваш UI должен сделать одно нажатие надёжным, даже если сеть плоха.

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

Хороший дефолт для критичных действий — явное состояние ожидания. После первого нажатия сразу переключите основную кнопку в состояние «Отправляем…», отключите её и покажите короткую строку с объяснением.

Паттерны, которые работают при нестабильной сети:

  • Отключайте основное действие после нажатия и держите его отключённым до финального результата.
  • Показывайте видимый статус «В ожидании» с деталями (сумма, получатель, количество).
  • Добавьте «Недавнюю активность», чтобы пользователь мог подтвердить отправленное.
  • При сворачивании приложения сохраняйте статус pending при возврате.
  • Предпочитайте одну чёткую основную кнопку вместо нескольких целей на одном экране.

Иногда запрос проходит, но ответ теряется. Рассматривайте это как нормальный исход, а не ошибку, которая провоцирует повтор. Вместо «Не удалось, попробуйте снова» покажите «Пока не ясно» и предложите безопасный следующий шаг — «Проверить статус». Если проверить невозможно, держите локальную запись и скажите пользователю, что вы обновите результат при восстановлении сети.

Сделайте «Попробовать снова» явным и безопасным. Показывайте его только тогда, когда можно повторить запрос, используя тот же локальный ID или ключ идемпотентности.

Реалистичный пример: нестабильная отправка заказа

Размещать логику там, где ей место
Переносите бизнес-правила на сервер, чтобы повторы оставались безопасными, а дубликаты — проще предотвращать.
Построить бэкенд

Клиент в поезде с переменным сигналом добавляет товары в корзину и нажимает «Оплатить». Приложение должно быть терпеливым, но не допускать двойных заказов.

Безопасная последовательность:

  1. Приложение создаёт клиентский attempt ID и отправляет запрос с idempotency key (UUID, сохранённый вместе с корзиной).
  2. Запрос ждёт connect timeout, затем более длинный read timeout. Поезд уходит в тоннель, и вызов таймаутится.
  3. Приложение повторяет попытку один раз, но только после короткой задержки и только если не получило ответа от сервера.
  4. Сервер получает второй запрос с тем же idempotency key и возвращает оригинальный результат вместо создания нового заказа.
  5. Приложение показывает финальный экран подтверждения, даже если ответ пришёл от повтора.

Кэширование строгое. Списки товаров, варианты доставки и налоговые таблицы можно кешировать кратко (GET). Отправка заказа (POST) никогда не кешируется. Даже при наличии HTTP‑кеша рассматривайте его как помощь для просмотра, а не как память о платеже.

Предотвращение дублей — смесь сетевых и UI‑решений. Когда пользователь нажимает «Оплатить», кнопка блокируется и экран показывает «Отправка заказа...» с одной кнопкой «Отмена». Если сеть теряется, переключитесь на «Все ещё пытаемся» и сохраните тот же attempt ID. Если пользователь принудительно закроет и откроет приложение, вы можете восстановиться, проверив статус заказа по сохранённому ID, вместо того чтобы предлагать оплатить снова.

Быстрый чеклист и следующие шаги

Если ваше приложение «вроде нормально» в офисном Wi‑Fi, но разваливается в поездах, лифтах или сельской местности, считайте это блокером перед релизом. Эта работа меньше про хитрый код и больше про ясные правила, которые можно повторять.

Чеклист перед релизом:

  • Задать таймауты по типам эндпоинтов (логин, лента, загрузка, checkout) и протестировать на ограниченной и высоколатентной сети.
  • Повторять только там, где безопасно, и ограничить с backoff (пара попыток для чтений, обычно ничего для записей).
  • Добавить idempotency key для каждой критичной записи (платежи, заказы, отправка форм), чтобы повтор или двойное нажатие не создали дубликаты.
  • Явно прописать правила кэширования: что можно отдавать устаревшим, что должно быть свежим и что никогда не кешировать.
  • Сделать состояния видимыми: pending, failed и completed должны выглядеть по‑разному, и приложение должно помнить завершённые действия после перезапуска.

Если хоть одна из этих пунктов «решим позже», вы получите разное поведение по экранам.

Следующие шаги, чтобы закрепить это

Напишите одностраничную сетевую политику: категории эндпоинтов, целевые таймауты, правила повторов и ожидания кэша. Примените её в одном месте (interceptors, shared client factory или небольшая обёртка), чтобы по умолчанию все члены команды получали одинаковое поведение.

Затем сделайте короткую практическую проверку дубликатов. Выберите одно критичное действие (например, checkout), симулируйте замороженный спиннер, принудительно закройте приложение, переключите режим полёта и нажмите кнопку снова. Если вы не сможете доказать безопасность, пользователи рано или поздно найдут способ сломать это.

Если вы хотите реализовать одинаковые правила и на бэкенде, и на клиентах без ручной привязки каждого эндпоинта, AppMaster (appmaster.io) может помочь, генерируя production‑ready бэкенд и нативные мобильные исходники. Даже в этом случае ключевое — политика: определите идемпотентность, правила повторов, кэширования и состояния UI один раз и применяйте их последовательно по всему потоку.

Вопросы и ответы

Что нужно сделать в первую очередь перед настройкой таймаутов и повторов?

Начните с определения того, что «правильно» для каждого экрана и действия — особенно для того, что должно произойти не более одного раза, например платежи или заказы. Как только правила будут ясны, настройте таймауты, повторы, кэширование и состояния UI в соответствии с ними, а не полагайтесь на дефолтные значения библиотек.

Какие симптомы чаще всего замечают пользователи при медленных или нестабильных сетях?

Пользователи обычно видят бесконечные спиннеры, ошибки после долгого ожидания, действия, которые срабатывают со второго раза, или дубли — два заказа или двойное списание. Чаще всего это результат неясных правил по повтору и состояниям «в ожидании» vs «ошибка», а не только плохого сигнала.

Как думать о connect, read и write таймаутах на мобильных устройствах?

Connect timeout — сколько ждать установки соединения; write timeout — сколько ждать отправки тела запроса (загрузки); read timeout — сколько ждать ответа после отправки запроса. Для низкорискованных чтений используйте более короткие таймауты, для критичных операций — более длинные, и всегда устанавливайте лимит в UI, чтобы пользователь не застрял в ожидании навсегда.

Если я могу задать только один таймаут в OkHttp, какой выбрать?

Да. Если можно задать только один, используйте callTimeout, чтобы ограничить всю операцию целиком и избежать «вечного» ожидания. При возможности добавьте отдельные connect/read/write таймауты для более тонкого контроля, например для загрузок или медленных потоков ответа.

Какие ошибки обычно безопасно повторять, а какие нет?

Повторять стоит только временные сбои: обрывы соединения, таймауты, DNS-проблемы и иногда 502/503/504. Не повторяйте 4xx, и не автоматизируйте повтор для операций записи, если у вас нет защиты идемпотентности — иначе вы рискуете создать дубликаты.

Как добавить повторы, не заставляя приложение зависать?

Используйте небольшое число попыток (обычно 1–3) с экспоненциальным backoff и небольшим джиттером, чтобы устройства не повторяли одновременно. Также задайте верхнюю границу общего времени на все повторы, чтобы показать пользователю очевидный результат вместо минутного спиннера.

Что такое идемпотентность и почему она важна для платежей и заказов?

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

Как генерировать и хранить ключ идемпотентности на Android?

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

Какие правила кэширования безопаснее всего применять в мобильных приложениях при ненадёжном соединении?

Кэшируйте только то, что можно допустить слегка устаревшим, и заставляйте проверять актуальность для денег, безопасности и финальных решений. Для чтений лучше короткая свежесть + валидация (ETag); для записей — не кэшировать, а для чувствительных ответов использовать no-store.

Какие паттерны UI уменьшают двойные нажатия и случайные повторные отправки при медленных сетях?

Отключайте основную кнопку после первого нажатия, показывайте состояние «Отправляем...» и видимый статус ожидания, который переживёт сворачивание или перезапуск. Если ответ мог потеряться, не провоцируйте пользователя на повторные нажатия: покажите «Мы пока не уверены» и предложите безопасный следующий шаг, например «Проверить статус».

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster