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

复用代码实现PDF文档查看、分享与保存的优化方案问询

稳定实现PDF查看、保存、分享的Fragment方案

一、替换AsyncTask:用Coroutine+OkHttp加载PDF到PdfView

AsyncTask已被废弃,改用协程+OkHttp实现异步流加载,更稳定且符合现代Android开发规范。假设你使用的是com.github.barteksc:android-pdf-viewer库:

1. 依赖配置(build.gradle)

implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'

2. Fragment中加载PDF的代码

class TermsFragment : Fragment() {
    private lateinit var pdfView: PDFView
    private val okHttpClient by lazy { OkHttpClient() }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_terms, container, false)
        pdfView = view.findViewById(R.id.pdfView)
        loadPdfFromUrl("https://your-server-url/terms.pdf")
        return view
    }

    private fun loadPdfFromUrl(url: String) {
        viewLifecycleScope.launch(Dispatchers.IO) {
            val request = Request.Builder().url(url).build()
            runCatching {
                okHttpClient.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) throw IOException("请求失败: ${response.code}")
                    response.body?.byteStream()?.let { inputStream ->
                        withContext(Dispatchers.Main) {
                            pdfView.fromStream(inputStream)
                                .enableSwipe(true)
                                .swipeHorizontal(false)
                                .enableDoubletap(true)
                                .load()
                        }
                    }
                }
            }.onFailure { e ->
                withContext(Dispatchers.Main) {
                    Toast.makeText(requireContext(), "PDF加载失败: ${e.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

二、稳定实现PDF保存(DownloadManager+正确广播监听)

之前广播监听不稳定的核心问题是未正确匹配下载ID、解析文件Uri,以下是标准化实现:

1. 保存PDF的方法

private fun savePdf(url: String): Long {
    val request = DownloadManager.Request(Uri.parse(url))
        .setMimeType("application/pdf")
        .setTitle("用户条款.pdf")
        .setDescription("正在保存用户条款")
        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        // 保存到应用私有外部目录,Android 10+无需额外权限
        .setDestinationInExternalFilesDir(requireContext(), Environment.DIRECTORY_DOCUMENTS, "用户条款.pdf")

    val downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    return downloadManager.enqueue(request)
}

2. 监听下载完成的BroadcastReceiver

在Fragment中注册广播,绑定生命周期避免内存泄漏:

private lateinit var downloadReceiver: BroadcastReceiver
private var downloadId: Long = -1

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 绑定保存按钮点击事件
    view.findViewById<Button>(R.id.btn_save).setOnClickListener {
        downloadId = savePdf("https://your-server-url/terms.pdf")
    }

    // 初始化下载完成广播接收器
    downloadReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val receivedId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
            if (receivedId != downloadId) return

            val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            val query = DownloadManager.Query().setFilterById(receivedId)
            val cursor = downloadManager.query(query)
            
            if (cursor.moveToFirst()) {
                val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
                if (status == DownloadManager.STATUS_SUCCESSFUL) {
                    val uriString = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
                    val fileUri = Uri.parse(uriString)
                    Toast.makeText(context, "PDF已保存: ${fileUri.path}", Toast.LENGTH_LONG).show()
                    // 如需自动分享,直接调用分享方法
                    sharePdf(fileUri)
                } else {
                    Toast.makeText(context, "PDF保存失败", Toast.LENGTH_SHORT).show()
                }
            }
            cursor.close()
        }
    }
    requireContext().registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}

override fun onDestroyView() {
    super.onDestroyView()
    // 注销广播,避免内存泄漏
    requireContext().unregisterReceiver(downloadReceiver)
}

三、稳定的PDF分享功能

必须在下载完成后拿到有效Uri再启动分享,使用FileProvider适配Android 7+的文件访问限制:

1. 配置FileProvider(AndroidManifest.xml)

<application>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

2. 创建res/xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="documents" path="Documents/" />
</paths>

3. 分享PDF的方法

private fun sharePdf(fileUri: Uri) {
    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        type = "application/pdf"
        // 将文件Uri转换为ContentUri,适配Android 7+
        val contentUri = FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", File(fileUri.path!!))
        putExtra(Intent.EXTRA_STREAM, contentUri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    startActivity(Intent.createChooser(shareIntent, "分享用户条款"))
}

四、封装可复用工具类

将加载、保存、分享逻辑封装到PDFUtils,方便多Fragment复用:

object PDFUtils {
    fun loadPdfFromUrl(url: String, pdfView: PDFView, lifecycleScope: LifecycleCoroutineScope, context: Context) {
        val okHttpClient = OkHttpClient()
        lifecycleScope.launch(Dispatchers.IO) {
            val request = Request.Builder().url(url).build()
            runCatching {
                okHttpClient.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) throw IOException("请求失败: ${response.code}")
                    response.body?.byteStream()?.let { inputStream ->
                        withContext(Dispatchers.Main) {
                            pdfView.fromStream(inputStream)
                                .enableSwipe(true)
                                .swipeHorizontal(false)
                                .enableDoubletap(true)
                                .load()
                        }
                    }
                }
            }.onFailure { e ->
                withContext(Dispatchers.Main) {
                    Toast.makeText(context, "PDF加载失败: ${e.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    fun savePdf(url: String, context: Context): Long {
        val request = DownloadManager.Request(Uri.parse(url))
            .setMimeType("application/pdf")
            .setTitle("用户条款.pdf")
            .setDescription("正在保存用户条款")
            .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
            .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOCUMENTS, "用户条款.pdf")

        val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        return downloadManager.enqueue(request)
    }

    fun sharePdf(fileUri: Uri, context: Context) {
        val shareIntent = Intent(Intent.ACTION_SEND).apply {
            type = "application/pdf"
            val contentUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", File(fileUri.path!!))
            putExtra(Intent.EXTRA_STREAM, contentUri)
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        }
        context.startActivity(Intent.createChooser(shareIntent, "分享用户条款"))
    }

    fun getDownloadedFileUri(downloadId: Long, context: Context): Uri? {
        val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val query = DownloadManager.Query().setFilterById(downloadId)
        val cursor = downloadManager.query(query)
        return if (cursor.moveToFirst()) {
            val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
            if (status == DownloadManager.STATUS_SUCCESSFUL) {
                val uriString = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
                Uri.parse(uriString)
            } else null
        } else null.also { cursor.close() }
    }
}

Fragment中调用工具类

// 加载PDF
PDFUtils.loadPdfFromUrl("https://your-server-url/terms.pdf", pdfView, viewLifecycleScope, requireContext())

// 保存PDF
downloadId = PDFUtils.savePdf("https://your-server-url/terms.pdf", requireContext())

// 分享PDF(在广播接收器中)
PDFUtils.getDownloadedFileUri(downloadId, requireContext())?.let {
    PDFUtils.sharePdf(it, requireContext())
}

关键注意事项

  • 权限处理:Android 10+无需申请WRITE_EXTERNAL_STORAGE权限;若需保存到公共目录,Android 11+需申请MANAGE_EXTERNAL_STORAGE。
  • 生命周期管理:广播接收器在onDestroyView中注销,协程使用viewLifecycleScope,确保Fragment销毁时自动清理资源。
  • 错误捕获:所有网络请求和文件操作添加异常处理,避免崩溃。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 22:40:57