如何在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
相关产品推荐
相关产品推荐

