Ever since I migrated 3DCollection to KMP I’ve been getting a steady stream of OOM crashes in Crashlytics. I never paid much attention because they were always on low-memory devices — but they bothered me.
Until one update I decided to actually fix it. At first I thought the file wasn’t being saved to disk correctly, so I added a write channel. Crashes kept coming. Then I started suspecting Ktor’s HttpClient and, reading the documentation with more patience than I should have had from the start, I found it.
The problem
File downloads were using Ktor’s client.get():
val response = service.client.get(urlDownload)
What I didn’t know is that .get() loads the entire response body into memory. Download a 20MB PDF → 20MB in RAM. On a device with 1GB of RAM shared between the OS and all running apps, that’s enough to blow it up.
The fix
The solution is prepareGet().execute { }, which opens a streaming channel and writes directly to the destination without going through memory:
service.client.prepareGet(urlDownload).execute { response ->
val channel = response.bodyAsChannel()
// write channel directly to sink/file
}
It’s a simple change, but you wouldn’t know without reading the docs — .get() looks like the obvious choice. With .get() you get all the bytes in memory. With prepareGet().execute { } you get a channel that flows directly to disk.
What I take away
Two things. The first is to try to understand better and question whether what you always do might sometimes be wrong — or simply not designed for that use case. The second is that crashes that look stupid break your brain, but at the same time, having zero OOM crashes is a great feeling.