Mobile DevelopmentiOSAndroidLLM APIStreaming

Mobile AI App Development: iOS & Android LLM API Integration

1 min read

LLM API docs are written for servers. Your curl one-liner works in a terminal. Your Python SDK runs fine in a notebook.

Try that same call from a phone. Cellular handoffs drop your stream mid-response. Background task limits kill long generations. And the API key you bundled into the binary? Anyone with a free disassembler can extract it — full account access, every model, a $14,000 bill before you even notice.

This guide covers both platforms: Swift and Kotlin implementations with working streaming code, on-device/cloud hybrid routing, and the proxy architecture that keeps credentials on your server, never in your app binary.


Two Integration Approaches

Firebase AI Logic (Managed SDK)

Google’s official SDK for calling Gemini from mobile apps. Handles authentication, streaming, and on-device/cloud hybrid routing. Supports Swift (iOS), Kotlin/Java (Android), Dart (Flutter), and JavaScript (Web).

Pros: Managed authentication — API key lives server-side behind Firebase App Check. Built-in streaming support. On-device fallback with Gemini Nano on supported Android devices. Cons: Gemini-only. If you need GPT, Claude, or DeepSeek, you need a different approach.

Direct REST API (OpenAI-Compatible Endpoint)

Call any OpenAI-compatible endpoint directly from the mobile app via HTTP. Works with any provider through a unified API platform — one base URL, one API key format, one request/response structure.

Pros: Provider-agnostic. Access to every model through one integration. Cons: You’re responsible for API key security. You handle streaming, retry, and error handling yourself. More code. More control.

The compromise: use a thin backend proxy. The mobile app authenticates with your backend. Your backend holds the API key and forwards requests to the LLM provider. The mobile app never sees a credential that gives direct API access. This secure backend proxy pattern is the foundation of mobile API security — keep credentials server-side, authenticate users, not apps.


Swift (iOS) Implementation

import Foundation

class LLMClient {
    private let baseURL: URL
    private let apiKey: String
    private let session: URLSession

    init(baseURL: URL, apiKey: String) {
        self.baseURL = baseURL
        self.apiKey = apiKey
        self.session = URLSession(configuration: .default)
    }

    func streamChat(messages: [[String: String]], model: String) -> AsyncThrowingStream<String, Error> {
        AsyncThrowingStream { continuation in
            var request = URLRequest(url: baseURL.appendingPathComponent("chat/completions"))
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")

            let body: [String: Any] = [
                "model": model,
                "messages": messages,
                "stream": true
            ]
            request.httpBody = try? JSONSerialization.data(withJSONObject: body)

            let task = session.dataTask(with: request) { data, response, error in
                if let error = error {
                    continuation.finish(throwing: error)
                    return
                }
                // Parse SSE stream — each line is "data: {...}\n\n"
                guard let data = data else { return }
                let lines = String(data: data, encoding: .utf8)?.components(separatedBy: "\n\n") ?? []
                for line in lines where line.hasPrefix("data: ") {
                    let jsonStr = String(line.dropFirst(6))
                    if jsonStr == "[DONE]" { continuation.finish(); return }
                    if let token = self.extractToken(from: jsonStr) {
                        continuation.yield(token)
                    }
                }
            }
            task.resume()
        }
    }
}

Key mobile-specific concerns: use AsyncThrowingStream for Swift Concurrency compatibility, handle background task expiration for long streams, and cache the URLSession — creating a new session per request leaks memory on iOS. For the complete request/response format reference including streaming parameters, see the chat completions API documentation.


Kotlin (Android) Implementation

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody

class LLMClient(private val baseUrl: String, private val apiKey: String) {
    private val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(120, TimeUnit.SECONDS)  // Long generations need long read timeouts
        .build()

    fun streamChat(messages: List<Map<String, String>>, model: String): Flow<String> = flow {
        val json = JSONObject().apply {
            put("model", model)
            put("messages", JSONArray(messages))
            put("stream", true)
        }

        val request = Request.Builder()
            .url("$baseUrl/chat/completions")
            .header("Authorization", "Bearer $apiKey")
            .header("Content-Type", "application/json")
            .post(json.toString().toRequestBody("application/json".toMediaType()))
            .build()

        client.newCall(request).execute().use { response ->
            response.body?.charStream()?.buffered()?.lineSequence()?.forEach { line ->
                if (line.startsWith("data: ") && line != "data: [DONE]") {
                    val token = extractToken(line.removePrefix("data: "))
                    if (token != null) emit(token)
                }
            }
        }
    }
}

On Android: use StateFlow or SharedFlow for UI state updates from streaming. Configure readTimeout generously — long LLM generations can exceed default HTTP timeouts. Use WorkManager for requests that need to complete even if the app is backgrounded.


On-Device + Cloud Hybrid

Not every AI task needs a cloud API call. Simple tasks run locally. Complex tasks reach the cloud. The same tiered routing logic that powers server-side architectures works on mobile.

iOS: Apple’s Foundation Models framework (iOS 26+) provides a ~3B parameter on-device model for summarization, rewriting, and basic Q&A. No network call. Zero latency. Data never leaves the device.

Android: Gemini Nano via AI Core provides on-device inference on supported devices (Pixel 9+, Galaxy S25+, Xiaomi 15+, OnePlus 13+). Dedicated endpoints for summarization and rewriting.

The hybrid pattern: try on-device first. If the local model returns low confidence or the task exceeds its capability, fall back to cloud API. The user gets instant responses for simple tasks and full capability for complex ones — without a network call for every interaction. For the complete server-side counterpart of this architecture, see the hybrid deployment guide. For reducing the input token cost of repeated system prompts on cloud fallback calls, see our prompt caching strategies.


Mobile-Specific Disasters: Three Scenarios That Will Bite You

Mobile AI development has failure modes that backend teams never encounter. Here are three that have burned teams I have worked with.

Cellular Network Drops Mid-Stream

A fintech app used streaming LLM responses for real-time spending analysis. In the office, on WiFi, the feature worked flawlessly — 15-second streams completing without a single dropped token. The QA team signed off.

Launch week: 23% of cellular users received truncated responses. Not because the API failed — because the user walked past a dead zone, or switched from 5G to 4G, or entered an elevator. The TCP connection dropped. The stream ended. The user saw half a response and hit “retry,” doubling the API cost for that interaction. Each retry consumed tokens for the full conversation prefix again.

The fix: implement request hedging. Detect a stream interruption and immediately reconnect from the last received token position, not from the beginning. Use NSURLSession background configuration with waitsForConnectivity enabled on iOS. On Android, register a ConnectivityManager.NetworkCallback to detect transitions and checkpoint stream state before the switch completes. Most importantly, never QA streaming exclusively on WiFi — cellular testing with packet loss simulation (Android’s Dev Tools network throttling, iOS’s Network Link Conditioner) must be part of the test plan.

API Keys Extracted from App Binaries

A startup shipped their iOS app with the OpenAI API key embedded in a Config.plist file. Within 72 hours, a user decompiled the IPA, extracted the key, and — according to the team’s postmortem — ran up $14,000 in usage charges calling GPT-4 for a crypto trading bot. The key had no rate limits, no usage caps, and no per-endpoint restrictions.

API keys in mobile binaries are not secrets. Reverse engineering tooling — jadx for Android APKs, Hopper or Ghidra for iOS IPAs — makes extraction trivial. Obfuscation is not security. String encryption in the binary is not security. If the app can decrypt the key at runtime, so can anyone with a debugger.

The only correct approach: never put an LLM API key in the app binary. Use a thin backend proxy that authenticates the user — not the app — and holds the API key server-side. Combine with Firebase App Check or Apple DeviceCheck to verify requests originate from your legitimate app (not a script replaying your API calls). Rate-limit per user, not per API key. If your backend proxy gets compromised, you rotate one key. If a thousand app binaries each contain the same key, you have a mass-rotation event. Per-user rate limiting and usage caps add a second line of defense — even a leaked credential cannot consume unlimited quota.

Background Task Termination Kills Your Stream

An AI journaling app generated daily summaries using a long LLM context window — 30 to 60 seconds of streaming per session. Users would start the generation, switch to another app while waiting, and return to find the summary unfinished. iOS had terminated the background URLSession task at the 30-second mark. Android’s Doze mode had throttled the connection to near-zero throughput.

On iOS, background URLSession tasks with the background configuration get a discretionary time budget — the OS decides when to allocate it. There is no guarantee your stream completes. On Android, the WorkManager longRunningWorker API (introduced in WorkManager 3.1) explicitly tells the OS “this task needs sustained execution” and displays a persistent notification. Without it, the OS may suspend your network I/O after 10 minutes of background execution at most — and aggressively optimized OEM implementations (looking at you, Chinese ROMs) may suspend it much sooner.

Design your streaming architecture to survive termination. Split long generations into multiple short requests with checkpoint markers. If the stream dies, resume from the last checkpoint — not from the beginning. For truly long-running AI tasks (document analysis, batch processing), push the work to a backend worker and let the app poll for results. The mobile app’s job is to submit the request and display the result. The server’s job is to do the heavy lifting without worrying about OS task schedulers.



Further reading. Mobile AI apps share integration patterns with the standard chat completions API. For cost control on mobile traffic, prompt caching dramatically reduces repeated system prompt costs.


FAQ

Is it safe to call LLM APIs directly from a mobile app?

No, if the API key gives broad access. An API key extracted from your app binary can be used to call any model, consume your quota, and run up your bill. Use a backend proxy: the app authenticates with your server, your server holds the API key and calls the LLM provider. The app never sees a credential with direct API access.

Firebase AI Logic vs. direct OpenAI-compatible API?

Firebase AI Logic: faster setup, managed security, Gemini-only. Direct API: provider flexibility, more control, more code. For prototypes and MVPs, Firebase AI Logic gets you to market faster. For production apps that need multi-model access, direct API through a backend proxy is the right architecture.

What are mobile streaming best practices?

Always use a background-capable network client. Streams can run for 10-60 seconds — users will background your app. If the OS kills your network connection, the stream dies and the user gets a partial response. Use URLSession background configuration on iOS. Use WorkManager with long-running workers on Android. Test streaming over cellular with simulated packet loss — WiFi development masks network reliability problems. For common mobile integration issues including authentication flows and error recovery patterns, see the troubleshooting resource.

Is on-device AI good enough to skip the cloud API?

For summarization, rewriting, and basic classification: yes. On-device models handle these reliably. For complex reasoning, multi-step tool use, and nuanced generation: no. The hybrid pattern — on-device first, cloud fallback — gives you the best of both. Start with on-device for latency and privacy. Escalate to cloud when capability is needed.

How do I handle API key rotation without forcing an app update?

Never put the API key in the app binary — it lives on your backend proxy. Rotation is then a server-side operation: generate a new key, update the proxy’s environment variable, revoke the old key. The app never knows the key changed. If you are using Firebase AI Logic, key rotation is handled by Google — your app authenticates with Firebase Auth, and the Firebase backend manages API credentials. The key takeaway: if key rotation requires an app store release and a user update cycle, your architecture is wrong. The mobile app should authenticate the user — not the API — and the server should hold the credential that the user never sees.


Mobile AI development is backend AI development plus network resilience, battery awareness, and credential security. The API calls are the same. The environment is not. Test on cellular. Stream through background-capable clients. Never put an API key in an app binary. Everything else follows the same patterns as your server-side integration.

On-device, cloud, and every model in between — one endpoint that works wherever your users are. Provision your mobile AI backend on TokSpan — streaming-optimized endpoints and model fallback routing for iOS and Android. $5 free credits included.