ktor と okio を使って大きなファイルをダウンロードする

kyamada,Multiplatformktorokio

大きなファイルをダウンロードする場合、ファイルの内容をすべてメモリに展開してしまうと、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)
    }
}

参考

© 品川アプリ.RSS