ktor と okio を使って大きなファイルをダウンロードする
大きなファイルをダウンロードする場合、ファイルの内容をすべてメモリに展開してしまうと、OutOfMemoryError が発生してしまいます。
この記事では、ktor (opens in a new tab) と okio (opens in a new tab) を使って、バッファを使ってメモリを節約しつつ、ファイルに書き込む方法を説明します。
コードはMultiplatformに対応しており、AndroidとiOSで動作確認しています。
API クライアントの実装
ExampleApi.kt
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.HttpResponse
import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.core.isEmpty
import io.ktor.utils.io.core.readBytes
import kotlinx.serialization.json.Json
import okio.FileSystem
import okio.Path
import okio.SYSTEM
class ExampleApi {
private val httpClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
useAlternativeNames = false
})
}
}
suspend fun downloadExampleZip(filePath: Path) {
httpClient.prepareGet("https://example.com/example.zip")
.execute { httpResponse ->
writeFile(httpResponse, filePath)
}
}
private suspend fun writeFile(httpResponse: HttpResponse, filePath: Path) {
val channel: ByteReadChannel = httpResponse.body()
FileSystem.SYSTEM.write(filePath) {
while (!channel.isClosedForRead) {
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
val bytes = packet.readBytes()
write(bytes)
}
}
}
}
companion object {
const val DEFAULT_BUFFER_SIZE = 4098
}
}
呼び出し例
Main.kt
suspend fun downloadExampleZip() {
withContext(Dispatchers.Default) {
// 注意: PlatformContext という自作クラスを作って、AndroidとiOSのそれぞれのディレクトリのパスを参照できるようにしています。
val zipFilePath = "${platformContext.cacheDir}/example.zip".toPath()
ExampleApi().downloadExampleZip(zipFilePath)
}
}