Mobile Image API: iOS, Android & React Native
Mobile apps thrive on dynamic visuals. Personalized greeting cards, share cards for social media, achievement badges, profile banners. These aren't things you can ship as static assets. You need to generate them on the fly, tailored to each user.
The problem? Mobile comes with constraints that web doesn't. Spotty connections, limited memory, battery concerns, and the ever-present risk of leaking API keys in a decompiled APK.
I've shipped image generation in iOS, Android, and React Native apps. Here's what actually works, with full code examples for all three platforms. If you're brand new to image generation APIs, start with how to generate images with an API first.
Architecture mobile to backend to image apiArchitecture: Mobile to Backend to Image API
Let's get this out of the way: don't call the image API directly from your mobile app in production.
Here's why. Any API key embedded in a mobile app can be extracted. It doesn't matter how clever you are about hiding it. Someone will decompile your app, run strings on the binary, and find it. For more on this, read our guide on image API security best practices.
The correct architecture looks like this:
Mobile App → Your Backend (holds API key) → Image Generation API
Your backend acts as a proxy. The mobile app sends a request to your server, your server calls the Imejis API with the secret key, and returns the generated image URL back to the app.
For prototyping and hackathons, calling the API directly is fine. Just don't ship it to the App Store that way.
Here's a minimal Express proxy to illustrate:
// backend/proxy.ts
import express from "express"
const app = express()
app.use(express.json())
app.post("/api/generate-image", async (req, res) => {
const { templateId, modifications } = req.body
const response = await fetch("https://api.imejis.io/v1/images", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IMEJIS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ templateId, modifications }),
})
const data = await response.json()
res.json({ imageUrl: data.imageUrl })
})Now let's build the mobile clients.
Ios swiftiOS (Swift)
Urlsession implementationURLSession Implementation
Swift's modern concurrency with async/await makes API calls clean. Here's a service class that talks to your backend proxy:
// ImageGenerationService.swift
import Foundation
struct GeneratedImage: Codable {
let imageUrl: String
}
struct ImageRequest: Codable {
let templateId: String
let modifications: [String: String]
}
class ImageGenerationService {
private let baseURL: URL
private let session: URLSession
init(baseURL: URL, session: URLSession = .shared) {
self.baseURL = baseURL
self.session = session
}
func generateImage(
templateId: String,
modifications: [String: String]
) async throws -> URL {
let endpoint = baseURL.appendingPathComponent("/api/generate-image")
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 20 // generous for mobile
let body = ImageRequest(
templateId: templateId,
modifications: modifications
)
request.httpBody = try JSONEncoder().encode(body)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw ImageGenerationError.serverError
}
let result = try JSONDecoder().decode(GeneratedImage.self, from: data)
guard let url = URL(string: result.imageUrl) else {
throw ImageGenerationError.invalidURL
}
return url
}
}
enum ImageGenerationError: Error {
case serverError
case invalidURL
case cacheMiss
}Notice the 20-second timeout. On mobile networks, especially cellular, you need more patience than on desktop.
Caching with urlcacheCaching with URLCache
iOS has a built-in caching layer that most developers underuse. Configure it once at app startup:
// AppDelegate.swift or App init
func setupCache() {
let memoryCapacity = 50 * 1024 * 1024 // 50 MB
let diskCapacity = 200 * 1024 * 1024 // 200 MB
let cache = URLCache(
memoryCapacity: memoryCapacity,
diskCapacity: diskCapacity,
diskPath: "image_cache"
)
URLCache.shared = cache
}For generated images, you'll also want your own mapping layer so you can look up cached images by template + parameters:
class ImageCacheManager {
static let shared = ImageCacheManager()
private let cache = NSCache<NSString, UIImage>()
private let fileManager = FileManager.default
private var cacheDirectory: URL {
fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
.appendingPathComponent("generated_images")
}
func cachedImage(for key: String) -> UIImage? {
// Check memory first
if let image = cache.object(forKey: key as NSString) {
return image
}
// Check disk
let fileURL = cacheDirectory.appendingPathComponent(key.sha256 + ".png")
guard let data = try? Data(contentsOf: fileURL),
let image = UIImage(data: data) else {
return nil
}
cache.setObject(image, forKey: key as NSString)
return image
}
func store(_ image: UIImage, for key: String) {
cache.setObject(image, forKey: key as NSString)
try? fileManager.createDirectory(
at: cacheDirectory,
withIntermediateDirectories: true
)
let fileURL = cacheDirectory.appendingPathComponent(key.sha256 + ".png")
try? image.pngData()?.write(to: fileURL)
}
}Swiftui image view componentSwiftUI Image View Component
Here's a reusable SwiftUI view that ties generation, loading, and caching together:
struct GeneratedImageView: View {
let templateId: String
let modifications: [String: String]
@State private var image: UIImage?
@State private var isLoading = false
@State private var error: Error?
private let service = ImageGenerationService(
baseURL: URL(string: "https://your-backend.com")!
)
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else if isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if error != nil {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
Text("Failed to load image")
.font(.caption)
Button("Retry") { Task { await loadImage() } }
}
} else {
Color.gray.opacity(0.1)
}
}
.task { await loadImage() }
}
private func loadImage() async {
let cacheKey = "\(templateId)_\(modifications.description)"
// Check cache first
if let cached = ImageCacheManager.shared.cachedImage(for: cacheKey) {
self.image = cached
return
}
isLoading = true
defer { isLoading = false }
do {
let imageURL = try await service.generateImage(
templateId: templateId,
modifications: modifications
)
let (data, _) = try await URLSession.shared.data(from: imageURL)
guard let uiImage = UIImage(data: data) else { return }
ImageCacheManager.shared.store(uiImage, for: cacheKey)
self.image = uiImage
} catch {
self.error = error
}
}
}Usage is dead simple:
GeneratedImageView(
templateId: "welcome-banner",
modifications: ["username": "Sarah", "level": "Pro"]
)
.frame(height: 200)Android kotlinAndroid (Kotlin)
Okhttp retrofit implementationOkHttp / Retrofit Implementation
On Android, Retrofit with coroutines is the standard approach. Here's the setup:
// ImageApiService.kt
import retrofit2.http.Body
import retrofit2.http.POST
data class ImageGenerationRequest(
val templateId: String,
val modifications: Map<String, String>
)
data class ImageGenerationResponse(
val imageUrl: String
)
interface ImageApiService {
@POST("/api/generate-image")
suspend fun generateImage(
@Body request: ImageGenerationRequest
): ImageGenerationResponse
}// ImageRepository.kt
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class ImageRepository {
private val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
private val api = Retrofit.Builder()
.baseUrl("https://your-backend.com")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ImageApiService::class.java)
suspend fun generateImage(
templateId: String,
modifications: Map<String, String>
): String {
val response = api.generateImage(
ImageGenerationRequest(templateId, modifications)
)
return response.imageUrl
}
}Caching with coilCaching with Coil
Coil is the modern image loading library for Android. It handles disk caching, memory caching, and lifecycle awareness out of the box. Configure it at the application level:
// MyApplication.kt
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
class MyApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(0.20) // 20% of app memory
.build()
}
.diskCache {
DiskCache.Builder()
.directory(cacheDir.resolve("generated_images"))
.maxSizeBytes(200L * 1024 * 1024) // 200 MB
.build()
}
.build()
}
}Jetpack compose componentJetpack Compose Component
Here's a Compose component that mirrors what we built in SwiftUI:
@Composable
fun GeneratedImage(
templateId: String,
modifications: Map<String, String>,
modifier: Modifier = Modifier
) {
val repository = remember { ImageRepository() }
var imageUrl by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
var hasError by remember { mutableStateOf(false) }
LaunchedEffect(templateId, modifications) {
isLoading = true
hasError = false
try {
imageUrl = repository.generateImage(templateId, modifications)
} catch (e: Exception) {
hasError = true
} finally {
isLoading = false
}
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
when {
isLoading -> {
CircularProgressIndicator()
}
hasError -> {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.Warning,
contentDescription = "Error"
)
Text("Failed to load", style = MaterialTheme.typography.bodySmall)
TextButton(onClick = {
// Trigger reload
hasError = false
isLoading = true
}) {
Text("Retry")
}
}
}
imageUrl != null -> {
AsyncImage(
model = imageUrl,
contentDescription = "Generated image",
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
}
}Use it like this:
GeneratedImage(
templateId = "welcome-banner",
modifications = mapOf("username" to "Sarah", "level" to "Pro"),
modifier = Modifier.height(200.dp).fillMaxWidth()
)React native typescriptReact Native (TypeScript)
Fetch backend proxyFetch + Backend Proxy
React Native's fetch works the same as web, so the API layer is straightforward:
// services/imageGeneration.ts
const API_BASE = "https://your-backend.com"
interface GenerateImageParams {
templateId: string
modifications: Record<string, string>
}
export async function generateImage(
params: GenerateImageParams
): Promise<string> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 20000)
try {
const response = await fetch(`${API_BASE}/api/generate-image`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
const data = await response.json()
return data.imageUrl
} finally {
clearTimeout(timeout)
}
}Fastimage component with cacheFastImage Component with Cache
react-native-fast-image handles caching and performance far better than the built-in Image component. Here's a full wrapper:
// components/GeneratedImage.tsx
import React, { useCallback, useEffect, useState } from "react"
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"
import FastImage from "react-native-fast-image"
import { generateImage } from "../services/imageGeneration"
interface Props {
templateId: string
modifications: Record<string, string>
style?: object
}
export function GeneratedImage({ templateId, modifications, style }: Props) {
const [imageUrl, setImageUrl] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const load = useCallback(async () => {
setLoading(true)
setError(false)
try {
const url = await generateImage({ templateId, modifications })
// Pre-cache the image
FastImage.preload([{ uri: url }])
setImageUrl(url)
} catch {
setError(true)
} finally {
setLoading(false)
}
}, [templateId, JSON.stringify(modifications)])
useEffect(() => {
load()
}, [load])
if (loading) {
return (
<View style={[style, { justifyContent: "center", alignItems: "center" }]}>
<ActivityIndicator />
</View>
)
}
if (error || !imageUrl) {
return (
<View style={[style, { justifyContent: "center", alignItems: "center" }]}>
<Text>Failed to load image</Text>
<TouchableOpacity onPress={load}>
<Text style={{ color: "#007AFF", marginTop: 8 }}>Retry</Text>
</TouchableOpacity>
</View>
)
}
return (
<FastImage
source={{
uri: imageUrl,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
style={style}
resizeMode={FastImage.resizeMode.contain}
/>
)
}Usage:
<GeneratedImage
templateId="welcome-banner"
modifications={{ username: "Sarah", level: "Pro" }}
style={{ height: 200, width: "100%" }}
/>For more on building React components around image generation, check out our guide on React image generation components.
Offline support and pre generationOffline Support and Pre-Generation
Mobile apps need to work when the signal drops. You can't generate images without a network connection, but you can prepare for offline use.
The strategy: pre-generate images when you have a good connection, then serve from cache.
Here's a practical approach for iOS:
class ImagePreloader {
private let service: ImageGenerationService
private let cacheManager = ImageCacheManager.shared
func preloadImagesIfNeeded() async {
// Only preload on WiFi
guard isOnWiFi() else { return }
let templates = [
("achievement-badge", ["type": "streak", "count": "7"]),
("achievement-badge", ["type": "streak", "count": "30"]),
("share-card", ["username": currentUser.name]),
]
for (templateId, mods) in templates {
let key = "\(templateId)_\(mods.description)"
guard cacheManager.cachedImage(for: key) == nil else { continue }
do {
let url = try await service.generateImage(
templateId: templateId,
modifications: mods
)
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
cacheManager.store(image, for: key)
}
} catch {
// Non-critical — skip and try again later
continue
}
}
}
private func isOnWiFi() -> Bool {
// Use NWPathMonitor to check connection type
// Return true only if on WiFi
}
}Call this from your app's background refresh handler or when the app enters the foreground on WiFi. The key insight is that generated images for known inputs (like achievement badges) are perfectly deterministic, so you can cache them forever.
Performance tips for mobilePerformance Tips for Mobile
Request the right image size. If you're displaying an image in a 300x200 card, don't request a 2048x1024 image. Most image APIs, including Imejis, let you specify dimensions. Smaller images mean faster downloads and less memory usage.
Pre-fetch during idle time. If you know the user will see a generated image on the next screen, start generating it as soon as the current screen loads. By the time they tap through, the image is ready.
Handle memory warnings. On iOS, listen for didReceiveMemoryWarning and clear your in-memory image cache. On Android, override onTrimMemory in your Application class. Let the disk cache survive. Only purge RAM.
// iOS memory management
NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { _ in
ImageCacheManager.shared.clearMemoryCache()
}// Android memory management
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= TRIM_MEMORY_MODERATE) {
imageLoader.memoryCache?.clear()
}
}Respect data saver modes. Both iOS and Android have low-data modes. Check for them and skip non-essential image generation when they're active. Your users will appreciate it.
For more strategies around API usage patterns and caching, see our guide on image API rate limits and caching.
Common patternsCommon Patterns
Here are the patterns I see most often in production mobile apps:
Share cards. When a user wants to share an achievement or content, generate a branded image card with their stats, profile photo, and your app's branding. These work great on Instagram Stories, Twitter, and iMessage.
Achievement badges. Gaming and fitness apps generate badges when users hit milestones. Pre-generate these since you know the possible badge types in advance. Fitness apps use workout cards with exercise stats, streaks, and progress visuals that users share on social media.
Personalized welcome screens. First-launch screens that greet the user by name with a custom illustration. Generate on signup, cache immediately.
Dynamic thumbnails. Content apps that generate preview images for user-created content (playlists, collections, mood boards). Generate when the content is created or updated, not when it's viewed.
The common thread: generate ahead of time whenever possible. Don't make the user wait for image generation in real-time unless the input is truly dynamic and unknown until that moment.
Get startedGet Started
The Imejis API works with all three platforms covered here. Create a template in the dashboard, grab your API key, set up a backend proxy, and you're generating images from mobile in minutes.
Start with the simplest use case, maybe a share card or welcome banner, and expand from there. The patterns in this guide scale well from a single image type to dozens.
Related tutorials:
- How to generate images with an API: start here if you're new
- React image generation components: deeper dive into React patterns
- Image API rate limits and caching: production caching strategies
- Image API security best practices: protecting your API keys
FaqFAQ
Should i call the image api directly from the mobile appShould I call the image API directly from the mobile app?
Ideally no. Use a backend proxy to keep your API key secure. If you must call directly (prototyping), use certificate pinning and obfuscate the key. Never hardcode keys in source.
How do i handle slow mobile connectionsHow do I handle slow mobile connections?
Set generous timeouts (15-30 seconds), show loading placeholders, cache aggressively, and pre-generate images when on WiFi. The API returns images in 1-2 seconds on good connections.
Can i generate images offlineCan I generate images offline?
No, the API requires an internet connection. But you can pre-generate and cache images while online, then serve from cache when offline. Core Data (iOS) or Room (Android) work well for this.
What image format should i requestWhat image format should I request?
The API returns PNG by default, which works on all platforms. For smaller file sizes on mobile, consider converting to WebP client-side after receiving the image.
How do i display the generated image efficientlyHow do I display the generated image efficiently?
On iOS, use URLSession with URLCache. On Android, use Coil or Glide with disk caching. React Native has react-native-fast-image. All handle caching, memory management, and placeholder display.