You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在KMM中调用返回流式PDF文件的API并下载文件?

KMM中基于Ktor-client实现流式PDF下载与保存

1. 共用层(Common Module)实现

1.1 定义数据结构

先定义数据类用于传递下载后的文件名和字节数据:

data class PdfDownloadResult(
    val fileName: String,
    val fileBytes: ByteArray
) {
    // 重写equals和hashCode,避免ByteArray默认引用比较问题
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PdfDownloadResult

        if (fileName != other.fileName) return false
        if (!fileBytes.contentEquals(other.fileBytes)) return false

        return true
    }

    override fun hashCode(): Int {
        var result = fileName.hashCode()
        result = 31 * result + fileBytes.contentHashCode()
        return result
    }
}

1.2 API调用与文件名解析

在共用层用Ktor发起请求,读取PDF字节流并从响应头提取文件名:

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpHeaders

class PdfApi(private val httpClient: HttpClient) {
    suspend fun downloadPdf(): Result<PdfDownloadResult> {
        return try {
            val response: HttpResponse = httpClient.get("你的PDF接口地址")
            
            // 解析Content-Disposition头中的文件名
            val fileName = extractFileName(response.headers[HttpHeaders.ContentDisposition])
                ?: throw IllegalArgumentException("响应头未包含有效文件名")
            
            // 读取PDF字节数据
            val pdfBytes = response.body<ByteArray>()
            
            Result.success(PdfDownloadResult(fileName, pdfBytes))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    // 从Content-Disposition头中提取文件名
    private fun extractFileName(contentDisposition: String?): String? {
        return contentDisposition?.let {
            val regex = Regex("filename=\"([^\"]+)\"")
            regex.find(it)?.groups?.get(1)?.value
        }
    }
}

2. 平台侧文件保存实现

注意:文件系统操作属于平台特有逻辑,需在对应平台模块单独实现

2.1 Android平台(Android Module)

实现文件保存逻辑,可选择应用私有目录或公共下载目录:

import android.content.Context
import java.io.File
import java.io.FileOutputStream

class AndroidFileSaver(private val context: Context) {
    suspend fun savePdf(result: PdfDownloadResult): Result<File> {
        return try {
            // 使用应用私有目录(无需权限),也可改为公共下载目录(需权限)
            val saveDir = context.getExternalFilesDir("pdfs") ?: context.filesDir
            saveDir.mkdirs() // 创建目录(不存在则创建)
            
            val targetFile = File(saveDir, result.fileName)
            FileOutputStream(targetFile).use { outputStream ->
                outputStream.write(result.fileBytes)
            }
            
            Result.success(targetFile)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

若保存到公共下载目录,Android 10+需使用MediaStore,低版本需申请WRITE_EXTERNAL_STORAGE权限。

2.2 iOS平台(iOS Module)

使用FileManager将文件保存到Documents目录:

import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSUserDomainMask

class IOSFileSaver {
    suspend fun savePdf(result: PdfDownloadResult): Result<NSURL> {
        return try {
            val fileManager = NSFileManager.defaultManager()
            // 获取Documents目录路径
            val documentsDir = fileManager.URLForDirectory(
                directory = NSDocumentDirectory,
                inDomain = NSUserDomainMask,
                appropriateForURL = null,
                create = false,
                error = null
            ) ?: throw IllegalStateException("无法获取Documents目录")
            
            val targetUrl = documentsDir.URLByAppendingPathComponent(result.fileName)
            val data = result.fileBytes.toNSData()
            
            val success = data.writeToURL(targetUrl, atomically = true)
            if (success) {
                Result.success(targetUrl)
            } else {
                throw IllegalStateException("文件保存失败")
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// 扩展函数:将Kotlin ByteArray转为iOS NSData
private fun ByteArray.toNSData(): platform.Foundation.NSData {
    return platform.Foundation.NSData.create(bytes = this, length = this.size.toULong())
}

3. 调用示例

3.1 Android侧调用

// 在ViewModel或CoroutineScope中执行
val pdfApi = PdfApi(yourHttpClientInstance)
val fileSaver = AndroidFileSaver(appContext)

lifecycleScope.launch {
    val downloadResult = pdfApi.downloadPdf()
    downloadResult.onSuccess { pdfResult ->
        val saveResult = fileSaver.savePdf(pdfResult)
        saveResult.onSuccess { savedFile ->
            // 文件保存成功,可打开或提示用户
        }.onFailure { e ->
            // 处理保存失败逻辑
        }
    }.onFailure { e ->
        // 处理下载失败逻辑
    }
}

3.2 iOS侧调用

// 在Swift代码中通过Task调用
let pdfApi = PdfApi(httpClient: yourHttpClientInstance)
let fileSaver = IOSFileSaver()

Task {
    let downloadResult = await pdfApi.downloadPdf()
    switch downloadResult {
    case .success(let pdfResult):
        let saveResult = await fileSaver.savePdf(pdfResult)
        switch saveResult {
        case .success(let fileUrl):
            // 文件保存成功,可进行后续操作
        case .failure(let error):
            // 处理保存失败
        }
    case .failure(let error):
        // 处理下载失败
    }
}

内容的提问来源于stack exchange,提问作者Aju

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 00:20:45