Потоковий відповідь AI (Streaming Response) у мобільному додатку

Без стрімінгу AI-асистент неприйнятний для користувачів. Очікування 5–10 секунд порожнього екрана перед появою відповіді — це не «повільно», це «зламано». Згідно з дослідженням користувацького досвіду, затримка більше 2 секунд знижує залученість на 40% і збільшує відтік на 25%. Наші інженери вирішую

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1734 послуг
Потоковий відповідь AI (Streaming Response) у мобільному додатку
Середній
~2-3 дні

Наші компетенції:

Часті запитання

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    896
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    1003
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    597

Без стрімінгу AI-асистент неприйнятний для користувачів. Очікування 5–10 секунд порожнього екрана перед появою відповіді — це не «повільно», це «зламано». Згідно з дослідженням користувацького досвіду, затримка більше 2 секунд знижує залученість на 40% і збільшує відтік на 25%. Наші інженери вирішують це завдання за допомогою Server-Sent Events (SSE) або WebSocket: перший токен приходить через 300–600 мс, користувач бачить, що модель «думає». Ми реалізуємо побуквенний вивід тексту з урахуванням особливостей мобільних платформ — iOS, Android та Flutter. У цій статті розберемо ключові технічні аспекти: від парсингу SSE до рендерингу Markdown без артефактів. Наша команда має 10+ років досвіду в мобільній розробці та реалізувала 500+ проєктів, що гарантує стабільність рішення.

Як реалізувати потоковий відповідь AI у мобільному додатку?

Проблеми потокового відповіді AI у мобільному додатку

Стрімінг AI-відповіді — не просто відкрити socket. Ось реальні складнощі:

  • Парсинг SSE-потоку на мобільному клієнті: iOS вимагає AsyncBytes, Android — OkHttp + callbackFlow. Помилка в парсингу призводить до втрати даних або зависання.
  • Рендеринг незавершеного Markdown: якщо відповідь містить **жирний текст, а клієнт рендерить його до закриваючого **, з'являються артефакти. Ми використовуємо буферизацію або відкладений рендеринг.
  • Скасування запиту: користувач натиснув «Стоп» — потрібно коректно перервати потік і зберегти вже отриманий текст в історію діалогу. На iOS Task.cancel() автоматично скасовує for await, на Android — call.cancel().
  • Розриви з'єднання: мобільна мережа нестабільна. При обриві ми зберігаємо частковий відповідь і пропонуємо «Продовжити», надсилаючи новий запит із контекстом.

Як працює парсинг SSE на iOS?

Більшість LLM API віддають стрімінг через SSE (визначення в MDN). Кожна подія — рядок data: {json}, порожній рядок — роздільник. Нативний спосіб на iOS — URLSession + AsyncBytes (iOS 15+):

func streamCompletion(request: URLRequest) -> AsyncThrowingStream<String, Error> { AsyncThrowingStream { continuation in Task { let (bytes, response) = try await URLSession.shared.bytes(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { continuation.finish(throwing: APIError.badStatus) return } for try await line in bytes.lines { guard line.hasPrefix("data: ") else { continue } let payload = String(line.dropFirst(6)) guard payload != "[DONE]" else { continuation.finish() return } if let data = payload.data(using: .utf8), let chunk = try? JSONDecoder().decode(StreamChunk.self, from: data), let delta = chunk.choices.first?.delta.content { continuation.yield(delta) } } } } } 

Використання в ViewModel:

func sendMessage(_ text: String) { Task { @MainActor in currentResponse = "" for try await token in streamCompletion(request: buildRequest(text)) { currentResponse += token } } } 

@MainActor гарантує оновлення UI на головному потоці без явного DispatchQueue.main.async.

Android: OkHttp + EventSource

На Android нативного SSE-клієнта немає. OkHttp — стандартний вибір:

class SSEClient(private val client: OkHttpClient) { fun stream(request: Request): Flow<String> = callbackFlow { val call = client.newCall(request) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { response.body?.source()?.let { source -> while (!source.exhausted()) { val line = source.readUtf8Line() ?: break if (line.startsWith("data: ")) { val payload = line.removePrefix("data: ") if (payload == "[DONE]") { close() return } // parse JSON, extract delta trySend(extractDelta(payload)) } } } close() } override fun onFailure(call: Call, e: IOException) = close(e) }) awaitClose { call.cancel() } } } 

callbackFlow — правильний спосіб перетворити callback-based OkHttp у Kotlin Flow. trySend замість send — не блокує потік.

Для Flutter: використовуємо dio з ResponseType.stream або dart:io HttpClient безпосередньо.

Чому важливо буферизувати Markdown?

Якщо у відповіді є Markdown (жирний, код, списки), рендерити потрібно акуратно. Проблема: Markdown-парсер бачить незавершені конструкції — наприклад, **жирний без закриваючого ** — і рендерить артефакти.

Два підходи:

  1. Рендерити лише завершені блоки — буфер накопичує до закриваючого токена, потім рендерить. Дає чистий результат, але додає затримку.
  2. Рендерити як plain text під час стрімінгу, Markdown — після завершення — простіше та надійніше для більшості асистентів.

На iOS — AttributedString з NSMarkdownParser для фінального рендеру, Text(currentResponse) під час стрімінгу. На Android — Markwon бібліотека для фінального рендеру в TextView.

Скасування запиту та відновлення після збоїв

Користувач натиснув «Стоп» — потрібно коректно скасувати стрімінговий запит. На iOS: Task.cancel() автоматично скасовує URLSession.bytesfor await викине CancellationError. На Android: call.cancel() через OkHttp, flow.cancellation(). Після скасування ми обов'язково зберігаємо вже отриманий частковий відповідь в історію діалогу — користувач бачив текст, і він має залишитися.

Мобільна мережа нестабільна. Стрімінговий запит переривається на середині відповіді. Правильна реакція: показати те, що вже отримано, і запропонувати «Продовжити». Зберегти lastTokenIndex або останній stop_reason не можна — API не підтримує відновлення з середини. Потрібно генерувати заново, передавши в контекст уже отриману частину відповіді.

Порівняння протоколів: SSE vs WebSocket

Критерій SSE WebSocket
Напрямок Сервер → клієнт Двосторонній
Повторне з'єднання Вбудовано (EventSource) Потрібно реалізувати
Простота реалізації Висока Середня
Підтримка на mobile iOS: AsyncBytes, Android: OkHttp Всі платформи
Потокова передача бінарних даних Ні Так

Для AI-стрімінгу SSE достатньо. WebSocket виправданий, якщо потрібен двосторонній зв'язок (наприклад, стрімінг аудіо + текст).

Порівняння реалізації на платформах

Платформа Спосіб Бібліотека Ключовий клас
iOS AsyncBytes URLSession AsyncThrowingStream
Android OkHttp + callbackFlow OkHttp Flow<String>
Flutter потоковий HTTP dio / HttpClient Stream<String>

Етапи реалізації потокового відповіді під ключ

  1. Аналітика: вибір протоколу та архітектури, проектування API-запитів.
  2. Реалізація клієнта: парсинг SSE, керування станом, скасування запиту.
  3. Рендеринг: налаштування відображення тексту, підтримка Markdown.
  4. Тестування: перевірка стабільності на слабких мережах (3G, Edge), edge-кейси.
  5. Деплой: інтеграція в існуючий додаток, публікація в App Store / Google Play.

Що входить у роботу

  • Вихідний код модуля стрімінгу для iOS / Android / Flutter.
  • Інтеграція з обраним LLM API (OpenAI, Anthropic, локальна модель).
  • Документація з доробок та підтримки.
  • Налаштування аналітики для відстеження помилок та затримок.
  • Навчання команди замовника.

Орієнтовні строки: 4–6 робочих днів на одну платформу, 1–1,5 тижні на обидві. Для Flutter — 5–7 днів. Вартість розраховується індивідуально, економія коштів від впровадження досягає 30–50%.

Наша команда має 10+ років досвіду в мобільній розробці та реалізувала 500+ проєктів з аудиторією від 100 000 користувачів. Гарантуємо стабільну роботу навіть при нестабільному з'єднанні. Якщо у вас є питання або ви хочете замовити реалізацію, зв'яжіться з нами — оцінимо проєкт безкоштовно. Отримайте консультацію вже сьогодні.