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

设置ViewModel的Flow值时出现竞态条件,PDF首次加载失败

PDF加载异常问题:首次下载后显示"Pdf is not ready123",重启后正常渲染

问题描述

实现从网络下载PDF并保存至内部存储,本地已存在则跳过下载。首次下载PDF时出现异常:

  • 在renderPDF()中已将ViewModel的isLoading Flow设为true
  • 日志显示mPdfRenderer的pageCount大于0,但UI却显示“Pdf is not ready123”
  • 重启应用(本地已有PDF)则能正常渲染LazyColumn

相关代码

class MainActivity : ComponentActivity() {

    private var file: File? = null
    private var mFileDescriptor: ParcelFileDescriptor? = null
    private var mPdfRenderer: PdfRenderer? = null
    var mCurrentPage: PdfRenderer.Page? = null

    private lateinit var myViewModel: MyViewModel

    var downloadID: Long = 0
    var downloadManager: DownloadManager? = null

    private fun renderPDF() {
        mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
        if (mFileDescriptor != null) {
            mPdfRenderer = PdfRenderer(mFileDescriptor!!)
            Log.i("pdfrenderd ", mPdfRenderer?.pageCount.toString())
            myViewModel.isLoaded()
        }
    }

    private fun isExternalStorageDocument(uri: Uri): Boolean {
        return "com.android.externalstorage.documents" == uri.authority
    }

    private fun isDownloadsDocument(uri: Uri): Boolean {
        return "com.android.providers.downloads.documents" == uri.authority
    }

    private fun isMediaDocument(uri: Uri): Boolean {
        return "com.android.providers.media.documents" == uri.authority
    }

    private fun isGooglePhotosUri(uri: Uri): Boolean {
        return "com.google.android.apps.photos.content" == uri.authority
    }

    private fun isGoogleDriveUri(uri: Uri): Boolean {
        return (
                "com.google.android.apps.docs.storage" == uri.authority ||
                        "com.google.android.apps.docs.storage.legacy" == uri.authority
                )
    }

    private fun File.copyInputStreamToFile(inputStream: InputStream) {
        this.outputStream().use { fileOut ->
            inputStream.copyTo(fileOut)
        }
    }

    suspend fun copyFileToInternalStorage(path: String) {
        val outputFile = File(filesDir, "mypdfs.pdf")
        outputFile.copyInputStreamToFile(File(path).inputStream())
        file = outputFile
        renderPDF()
    }

    private fun getMediaDocumentPath(
        context: Context,
        uri: Uri?,
        selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(column)
        try {
            if (uri == null) return null
            cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
            if (cursor != null && cursor.moveToFirst()) {
                val index = cursor.getColumnIndexOrThrow(column)
                return cursor.getString(index)
            }
        } finally {
            cursor?.close()
        }
        return null
    }

    private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val action = intent.action
            if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == action) {
                val fileUrl: Uri? = downloadManager?.getUriForDownloadedFile(downloadID)
                lifecycleScope.launch {
                    withContext(Dispatchers.IO) {
                        val path = getRealPathFromURI(fileUrl)
                        if (path != null) {
                            copyFileToInternalStorage(path)
                        }
                    }
                }
            }
        }
    }

    private fun getDriveFilePath(uri: Uri, context: Context): String? {
        val returnCursor = context.contentResolver.query(
            uri,
            null,
            null,
            null,
            null
        )
        val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        returnCursor.moveToFirst()
        val name = returnCursor.getString(nameIndex)
        returnCursor.close()

        val file = File(context.cacheDir, name)
        try {
            val inputStream = context.contentResolver.openInputStream(uri)
            val outputStream = FileOutputStream(file)
            var read = 0
            val maxBufferSize = 1 * 1024 * 1024
            val bytesAvailable = inputStream!!.available()

            val bufferSize = min(bytesAvailable, maxBufferSize)
            val buffers = ByteArray(bufferSize)
            while (inputStream.read(buffers).also { read = it } != -1) {
                outputStream.write(buffers, 0, read)
            }
            inputStream.close()
            outputStream.close()
        } catch (e: Exception) {
            context.unregisterReceiver(onDownloadComplete)
        }
        return file.path
    }

    private fun getFilePath(context: Context, uri: Uri?): String? {
        var cursor: Cursor? = null
        val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME)
        try {
            if (uri == null) return null
            cursor = context.contentResolver.query(
                uri,
                projection,
                null,
                null,
                null
            )
            if (cursor != null && cursor!!.moveToFirst()) {
                val index = cursor!!.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
                return cursor!!.getString(index)
            }
        } finally {
            cursor?.close()
        }
        return null
    }

    private fun getDataColumn(
        context: Context,
        uri: Uri?,
        selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(column)
        try {
            if (uri == null) return null
            cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
            if (cursor != null && cursor!!.moveToFirst()) {
                val index = cursor!!.getColumnIndexOrThrow(column)
                return cursor!!.getString(index)
            }
        } finally {
            cursor?.close()
        }
        return null
    }

    fun getRealPathFromURI(uri: Uri?): String? {
        when {
            DocumentsContract.isDocumentUri(this, uri) -> {
                when {
                    uri?.let { isExternalStorageDocument(it) } == true -> {
                        val docId = DocumentsContract.getDocumentId(uri)
                        val split = docId.split(":").toTypedArray()
                        val type = split[0]
                        return if ("primary".equals(type, ignoreCase = true)) {
                            if (split.size > 1) {
                                Environment.getExternalStorageDirectory()
                                    .toString() + "/" + split[1]
                            } else {
                                Environment.getExternalStorageDirectory().toString() + "/"
                            }
                        } else {
                            "storage" + "/" + docId.replace(":", "/")
                        }
                    }

                    uri?.let { isDownloadsDocument(it) } == true -> {
                        val fileName = getFilePath(this, uri)
                        if (fileName != null) {
                            return Environment.getExternalStorageDirectory()
                                .toString() + "/Download/" + fileName
                        }
                        var id = DocumentsContract.getDocumentId(uri)
                        if (id.startsWith("raw:")) {
                            id = id.replaceFirst("raw:".toRegex(), "")
                            val file = File(id)
                            if (file.exists()) return id
                        }
                        val contentUri = ContentUris.withAppendedId(
                            Uri.parse("content://downloads/public_downloads"),
                            java.lang.Long.valueOf(id)
                        )
                        return getDataColumn(this, contentUri, null, null)
                    }

                    uri?.let { isMediaDocument(it) } == true -> {
                        val doc docId = DocumentsContract.getDocumentId(uri)
                        val split = docId.split(":").toTypedArray()
                        val type = split[0]
                        val contentUri: Uri?
                        when (type) {
                            "image" -> {
                                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                            }

                            "video" -> {
                                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                            }

                            "audio" -> {
                                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                            }

                            else -> {
                                contentUri = MediaStore.Files.getContentUri("external")
                                val selection =
                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                                        MediaStore.MediaColumns.RELATIVE_PATH + "=?"
                                    } else {
                                        "_id=?"
                                    }
                                val selectionArgs = arrayOf(Environment.DIRECTORY_DOCUMENTS)
                                return getMediaDocumentPath(
                                    this,
                                    contentUri,
                                    selection,
                                    selectionArgs
                                )
                            }
                        }
                        val selection = "_id=?"
                        val selectionArgs = arrayOf(split[1])
                        return getDataColumn(this, contentUri, selection, selectionArgs)
                    }

                    uri?.let { isGoogleDriveUri(it) } == true -> {
                        return getDriveFilePath(uri, this)
                    }

                    else -> {
                        unregisterReceiver(onDownloadComplete)
                    }
                }
            }

            "content".equals(uri?.scheme, ignoreCase = true) -> {
                return if (uri?.let { isGooglePhotosUri(it) } == true) {
                    uri.lastPathSegment
                } else {
                    getDataColumn(
                        this,
                        uri,
                        null,
                        null
                    )
                }
            }

            "file".equals(uri?.scheme, ignoreCase = true) -> {
                return uri?.path
            }
        }
        return null
    }

    private fun downloadFile(url: String) {
        try {
            val uri = Uri.parse(url)
            val request = DownloadManager.Request(uri)

            request.setAllowedNetworkTypes(
                DownloadManager.Request.NETWORK_MOBILE or DownloadManager.Request.NETWORK_WIFI
            )

            request.setNotificationVisibility(
                DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
            )

            request.setDestinationInExternalFilesDir(
                this,
                Environment.DIRECTORY_DOCUMENTS,
                "myfile.pdf"
            )

            downloadManager =
                getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

            downloadID = downloadManager!!.enqueue(request)
        } catch (e: Exception) {
            unregisterReceiver(onDownloadComplete)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        registerReceiver(
            onDownloadComplete,
            IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
        )

        val outputFile = File(filesDir, "mypdfs.pdf")
        if (outputFile.exists()) {
            file = outputFile
            renderPDF()
        } else {
            downloadFile("https://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf")
        }

        setContent {
            PdfLazyTheme {

                var scale by remember { mutableStateOf(0.5f) }
                val loaded by myViewModel.isLoading.collectAsState()

                if (loaded) {
                    if ((mPdfRenderer?.pageCount ?: 0) <= 0) {
                        Text(text = "Pdf is not ready123")
                    } else {
                        LazyColumn() {
                            items(mPdfRenderer?.pageCount!!) { message ->
                                if (null != mCurrentPage) {
                                    mCurrentPage?.close()
                                }

                                mCurrentPage = mPdfRenderer?.openPage(message)

                                val bitmap = mCurrentPage?.width?.let {
                                    mCurrentPage?.height?.let { it1 ->
                                        Bitmap.createBitmap(
                                            it, it1,
                                            Bitmap.Config.ARGB_8888
                                        )
                                    }
                                }

                                bitmap?.let {
                                    mCurrentPage?.render(
                                        it,
                                        null,
                                        Matrix().apply { postRotate(0F) },
                                        PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
                                    )
                                }

                                Image(
                                    bitmap = bitmap!!.asImageBitmap(),
                                    contentDescription = "some useful description",
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .border(width = 1.dp, color = Color.Gray)
                                        .aspectRatio(1f)
                                        .pointerInput(Unit) {
                                            detectZoom { zoom ->
                                                scale *= zoom
                                            }
                                        }
                                        .graphicsLayer {
                                            scaleX = maxOf(1f, minOf(3f, scale));
                                            scaleY = maxOf(1f, minOf(3f, scale))
                                        }
                                        .zIndex(scale)
                                        .shadow(
                                            elevation = 4.dp,
                                            spotColor = Color.Gray,
                                        )
                                }
                            }
                        }
                    }
                } else {
                    Text(text = "Pdf is not ready")
                }
            }
        }
    }
}

class MyViewModel : ViewModel() {
    private val _isLoading = MutableStateFlow(true)
    val isLoading = _isLoading.asStateFlow()

    fun isLoaded(){
        _isLoading.value = true
    }
}

问题原因分析

  1. ViewModel状态逻辑错误:MyViewModel中_isLoading初始值为true,isLoaded()方法又将其设为true,导致UI在PDF未加载完成时就进入了loaded分支,此时mPdfRenderer还是null,触发“Pdf is not ready123”提示。重启时renderPDF()在onCreate同步执行,mPdfRenderer已初始化,所以正常显示。
  2. mPdfRenderer未纳入可观察状态:Compose UI不会监听Activity中局部变量mPdfRenderer的变化,首次下载完成后mPdfRenderer被赋值,但UI不会触发重组,依然读取旧的null值。

修复方案

1. 修正ViewModel状态逻辑

调整_isLoading初始值和isLoaded()方法,确保只有PDF加载完成后才标记为已加载:

class MyViewModel : ViewModel() {
    private val _isLoading = MutableStateFlow(false)
    val isLoading = _isLoading.asStateFlow()

    fun isLoaded(){
        _isLoading.value = true
    }
}

2. 将PdfRenderer转为可观察状态

在ViewModel中管理PdfRenderer,让Compose能感知其变化:

class MyViewModel : ViewModel() {
    private val _isLoading = MutableStateFlow(false)
    val isLoading = _isLoading.asStateFlow()
    
    private val _pdfRenderer = MutableStateFlow<PdfRenderer?>(null)
    val pdfRenderer = _pdfRenderer.asStateFlow()

    fun setPdfRenderer(renderer: PdfRenderer) {
        _pdfRenderer.value = renderer
        _isLoading.value = true
    }
}

修改renderPDF()中的调用:

private fun renderPDF() {
    mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    if (mFileDescriptor != null) {
        mPdfRenderer = PdfRenderer(mFileDescriptor!!)
        Log.i("pdfrenderd ", mPdfRenderer?.pageCount.toString())
        myViewModel.setPdfRenderer(mPdfRenderer!!)
    }
}

3. 调整UI逻辑,观察可观察状态

在Compose中收集pdfRenderer状态,替代直接读取Activity变量:

setContent {
    PdfLazyTheme {
        var scale by remember { mutableStateOf(0.5f) }
        val loaded by myViewModel.isLoading.collectAsState()
        val pdfRenderer by myViewModel.pdfRenderer.collectAsState()

        if (loaded) {
            if (pdfRenderer?.pageCount ?: 0 <= 0) {
                Text(text = "Pdf is not ready123")
            } else {
                LazyColumn {
                    items(pdfRenderer!!.pageCount) { pageIndex ->
                        val bitmap = remember(pageIndex) {
                            pdfRenderer!!.openPage(pageIndex).run {
                                Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
                                    render(it, null, Matrix().apply { postRotate(0F) }, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                                    close()
                                }
                            }
                        }

                        Image(
                            bitmap = bitmap.asImageBitmap(),
                            contentDescription = "PDF页面 $pageIndex",
                            modifier = Modifier
                                .fillMaxWidth()
                                .border(width = 1.dp, color = Color.Gray)
                                .aspectRatio(1f)
                                .pointerInput(Unit) {
                                    detectZoom { zoom ->
                                        scale *= zoom
                                    }
                                }
                                .graphicsLayer {
                                    scaleX = maxOf(1f,
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 23:53:11