Calling SnapAPI from Kotlin with OkHttp
Kotlin's coroutines and OkHttp's asynchronous HTTP client make calling SnapAPI clean and idiomatic. OkHttp's enqueue method provides async callback-based requests, while using it with coroutines and suspendCoroutine converts the callback interface to a clean suspend function. Add OkHttp to your build.gradle.kts dependencies along with the Kotlin coroutines library, and define a suspend function that builds the request, makes the call, and returns the response bytes.
// build.gradle.kts
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
}
// SnapApiClient.kt
import okhttp3.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import java.io.IOException
import java.net.URLEncoder
class SnapApiClient(private val apiKey: String) {
private val client = OkHttpClient.Builder()
.callTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
.build()
suspend fun screenshot(
url: String,
format: String = "png",
fullPage: Boolean = false,
viewportWidth: Int = 1280
): ByteArray {
val encodedUrl = URLEncoder.encode(url, "UTF-8")
val request = Request.Builder()
.url("https://api.snapapi.pics/screenshot?url=$encodedUrl&format=$format&full_page=$fullPage&viewport_width=$viewportWidth")
.header("Authorization", "Bearer $apiKey")
.get()
.build()
return suspendCancellableCoroutine { cont ->
val call = client.newCall(request)
cont.invokeOnCancellation { call.cancel() }
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
cont.resume(response.body!!.bytes())
} else {
cont.resumeWithException(IOException("HTTP ${response.code}"))
}
}
override fun onFailure(call: Call, e: IOException) = cont.resumeWithException(e)
})
}
}
}
// Usage in a coroutine scope
val client = SnapApiClient(System.getenv("SNAPAPI_KEY") ?: "")
val bytes = client.screenshot("https://example.com", format = "jpeg")
java.io.File("screenshot.jpg").writeBytes(bytes)
Android: Screenshot Display with Coil
In Android applications, load a screenshot asynchronously and display it in an ImageView or Jetpack Compose Image component using Coil or Glide. Rather than calling SnapAPI directly from the Android client (which would expose the API key in the APK), proxy the screenshot request through your backend and load the resulting image URL with Coil's AsyncImage. The backend generates a signed URL or a temporary proxy URL that Coil fetches, keeping the SnapAPI key server-side. For internal enterprise Android apps where key exposure risk is acceptable, use the SnapApiClient above directly with a ViewModel and LiveData or StateFlow to manage the coroutine lifecycle correctly.
// ScreenshotViewModel.kt (Android)
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ScreenshotViewModel : ViewModel() {
private val client = SnapApiClient(BuildConfig.SNAPAPI_KEY)
sealed class UiState {
object Loading : UiState()
data class Success(val bytes: ByteArray) : UiState()
data class Error(val message: String) : UiState()
}
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state
fun capture(url: String) {
viewModelScope.launch {
_state.value = UiState.Loading
_state.value = try {
val bytes = client.screenshot(url, format = "jpeg", viewportWidth = 1280)
UiState.Success(bytes)
} catch (e: Exception) {
UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// Compose UI
@Composable
fun ScreenshotScreen(viewModel: ScreenshotViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
when (val s = state) {
is ScreenshotViewModel.UiState.Loading -> CircularProgressIndicator()
is ScreenshotViewModel.UiState.Success -> {
val bitmap = BitmapFactory.decodeByteArray(s.bytes, 0, s.bytes.size)
Image(bitmap.asImageBitmap(), contentDescription = "Screenshot")
}
is ScreenshotViewModel.UiState.Error -> Text(s.message)
}
}
Ktor Server: Screenshot Proxy Endpoint
Ktor is JetBrains' asynchronous web framework for Kotlin, designed for building server-side applications and microservices. Use Ktor's built-in HttpClient with the CIO engine for async HTTP calls, and define a route that proxies screenshot requests to SnapAPI with the stored API key. Ktor's routing DSL and coroutines make the screenshot proxy endpoint concise and readable.
// Application.kt — Ktor screenshot proxy
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.net.URLEncoder
val httpClient = HttpClient(CIO) {
engine { requestTimeout = 60_000 }
}
val apiKey = System.getenv("SNAPAPI_KEY") ?: ""
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/screenshot") {
val url = call.request.queryParameters["url"]
?: return@get call.respond(HttpStatusCode.BadRequest, "url required")
val encoded = URLEncoder.encode(url, "UTF-8")
val response = httpClient.get(
"https://api.snapapi.pics/screenshot?url=$encoded&format=png"
) {
header("Authorization", "Bearer $apiKey")
}
call.respondBytes(
response.readBytes(),
ContentType.Image.PNG,
HttpStatusCode.OK
)
}
}
}.start(wait = true)
}
Coroutines Best Practices for Screenshot APIs
Kotlin coroutines provide powerful concurrency primitives for screenshot generation workflows. Use supervisorScope when capturing multiple URLs concurrently to prevent one failing capture from cancelling the remaining captures: unlike a regular coroutineScope where any child failure cancels all siblings, supervisorScope allows each child to fail independently. Combine supervisorScope with async and awaitAll to collect results from all captures, with each failure represented as an exception rather than a cancelled task. Use Dispatchers.IO for OkHttp-based screenshot calls since OkHttp is a blocking IO library wrapped in a suspend function. For Ktor's async HttpClient, use Dispatchers.Default since the engine is non-blocking. Apply structured concurrency principles throughout: always launch coroutines in a scope that is tied to the lifecycle of the component (ViewModelScope on Android, requestScope in Ktor handlers) to ensure coroutines are cancelled automatically when the component is destroyed, preventing resource leaks from abandoned screenshot requests.