复用代码实现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
相关产品推荐
相关产品推荐

