Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Displaying PDF Files in Jetpack Compose with Performance Optimization

Tech 2

Introduction

Displaying PDF files in Android applications using Jetpack Compose presents unique challenges, particularly when dealing with large documents. This article explores a comprehensive approach to rendering PDFs efficiently while managing memory consumption and user interface responsiveness.

Basic PDF Rendering Implementation

The foundation of PDF display in Android involves using the PdfRenderer class. Here's a basic implementation:

@Composable
fun PDFViewer(file: File) {
    val pdfDescriptor = ParcelFileDescriptor.open(
        file,
        ParcelFileDescriptor.MODE_READ_ONLY
    )
    
    val pdfRenderer = PdfRenderer(pdfDescriptor)
    
    LazyColumn {
        items(pdfRenderer.pageCount) { pageIndex ->
            val pageBitmap = renderPage(pdfRenderer, pageIndex)
            Image(
                bitmap = pageBitmap.asImageBitmap(),
                contentDescription = "PDF page ${pageIndex + 1}"
            )
        }
    }
}

private fun renderPage(
    renderer: PdfRenderer,
    pageIndex: Int
): Bitmap {
    return renderer.openPage(pageIndex).use { page ->
        val bitmap = Bitmap.createBitmap(
            page.width,
            page.height,
            Bitmap.Config.ARGB_8888
        )
        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
        bitmap
    }
}

This straightforward approach loads all pages simultaneously, which works for small PDFs but becomes problematic with larger documents.

Performance Issues with Basic Implementation

When testing with a 200MB PDF file, significant performance issues emerge:

  • Long loading times: All pages render at startup
  • High memory consumption: All bitmaps remain in memory
  • UI freezing: Interface remains unresponsive until all pages load

Performance Issues

Memory consumption can reach nearly 6GB for a PDF viewer application, which is unacceptable for production use.

Asynchronous Rendering with Coroutines

To address UI freezing and improve loading times, we can implement asynchronous rendering using Kotlin coroutines:

class PDFRendererManager(
    private val fileDescriptor: ParcelFileDescriptor
) {
    private val pdfRenderer = PdfRenderer(fileDescriptor)
    private val renderingMutex = Mutex()
    private val renderingScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
    
    val totalPages get() = pdfRenderer.pageCount
    
    private val pageCache = List(totalPages) { pageIndex ->
        PDFPage(
            pageIndex = pageIndex,
            pdfRenderer = pdfRenderer,
            coroutineScope = renderingScope,
            renderingLock = renderingMutex
        )
    }
    
    fun getPage(index: Int): PDFPage = pageCache[index]
    
    fun cleanup() {
        pageCache.forEach { it.releaseResources() }
        pdfRenderer.close()
        fileDescriptor.close()
    }
}

class PDFPage(
    private val pageIndex: Int,
    private val pdfRenderer: PdfRenderer,
    private val coroutineScope: CoroutineScope,
    private val renderingLock: Mutex
) {
    private var isRendered = false
    private var renderingJob: Job? = null
    
    val renderedContent = MutableStateFlow<Bitmap?>(null)
    
    fun renderPage() {
        if (!isRendered) {
            renderingJob = coroutineScope.launch {
                renderingLock.withLock {
                    val pageBitmap = pdfRenderer.openPage(pageIndex).use { page ->
                        val bitmap = Bitmap.createBitmap(
                            page.width,
                            page.height,
                            Bitmap.Config.ARGB_8888
                        )
                        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                        bitmap
                    }
                    isRendered = true
                    renderedContent.emit(pageBitmap)
                }
            }
        }
    }
    
    fun releaseResources() {
        isRendered = false
        renderedContent.value?.recycle()
        renderedContent.tryEmit(null)
    }
}

Optimized Compose Implementation

Here's how to integrate the asynchronous rendering with Jetpack Compose:

@Composable
fun PDFDocumentViewer(documentFile: File) {
    val pdfManager = remember {
        PDFRendererManager(
            ParcelFileDescriptor.open(
                documentFile,
                ParcelFileDescriptor.MODE_READ_ONLY
            )
        )
    }
    
    DisposableEffect(Unit) {
        onDispose { pdfManager.cleanup() }
    }
    
    LazyColumn {
        items(pdfManager.totalPages) { pageIndex ->
            val currentPage = pdfManager.getPage(pageIndex)
            
            LaunchedEffect(Unit) {
                currentPage.renderPage()
            }
            
            DisposableEffect(Unit) {
                onDispose { currentPage.releaseResources() }
            }
            
            val pageBitmap by currentPage.renderedContent.collectAsState()
            
            pageBitmap?.asImageBitmap()?.let { bitmap ->
                Image(
                    bitmap = bitmap,
                    contentDescription = "PDF page ${pageIndex + 1}",
                    modifier = Modifier.fillMaxWidth()
                )
            }
        }
    }
}

Memory Optimization with Page Dimensions

To further optimize memory usage, we can pre-calculate page dimentions and only render visible pages:

class OptimizedPDFPage(
    private val pageIndex: Int,
    private val pdfRenderer: PdfRenderer,
    private val coroutineScope: CoroutineScope,
    private val renderingLock: Mutex
) {
    private var isRendered = false
    private var renderingJob: Job? = null
    
    val pageDimensions = pdfRenderer.openPage(pageIndex).use { page ->
        PageSize(width = page.width, height = page.height)
    }
    
    val renderedContent = MutableStateFlow<Bitmap?>(null)
    
    fun calculateHeightForWidth(targetWidth: Int): Int {
        val aspectRatio = pageDimensions.width.toFloat() / pageDimensions.height
        return (targetWidth / aspectRatio).toInt()
    }
    
    fun renderPage() {
        if (!isRendered) {
            renderingJob = coroutineScope.launch {
                renderingLock.withLock {
                    val pageBitmap = pdfRenderer.openPage(pageIndex).use { page ->
                        val bitmap = Bitmap.createBitmap(
                            page.width,
                            page.height,
                            Bitmap.Config.ARGB_8888
                        )
                        val canvas = Canvas(bitmap)
                        canvas.drawColor(Color.WHITE)
                        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                        bitmap
                    }
                    isRendered = true
                    renderedContent.emit(pageBitmap)
                }
            }
        }
    }
    
    fun releaseResources() {
        isRendered = false
        renderedContent.value?.recycle()
        renderedContent.tryEmit(null)
    }
}

data class PageSize(val width: Int, val height: Int)

Final Optimized Implementation

Combining all optimizations, here's the complete solution:

@Composable
fun OptimizedPDFViewer(documentFile: File) {
    val pdfManager = remember {
        PDFRendererManager(
            ParcelFileDescriptor.open(
                documentFile,
                ParcelFileDescriptor.MODE_READ_ONLY
            )
        )
    }
    
    DisposableEffect(Unit) {
        onDispose { pdfManager.cleanup() }
    }
    
    LazyColumn {
        items(pdfManager.totalPages) { pageIndex ->
            val currentPage = pdfManager.getPage(pageIndex)
            
            BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
                val pageHeight = currentPage.calculateHeightForWidth(maxWidth.roundToPx())
                
                LaunchedEffect(Unit) {
                    currentPage.renderPage()
                }
                
                DisposableEffect(Unit) {
                    onDispose { currentPage.releaseResources() }
                }
                
                val pageBitmap by currentPage.renderedContent.collectAsState()
                
                pageBitmap?.asImageBitmap()?.let { bitmap ->
                    Image(
                        bitmap = bitmap,
                        contentDescription = "PDF page ${pageIndex + 1}",
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(pageHeight.dp)
                    )
                } ?: Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(pageHeight.dp)
                        .background(Color.LightGray)
                )
            }
        }
    }
}

Performance Results

After implementing these optimizations:

  • Significantly reduced loading time: Pages render only when needed
  • Controlled memory usage: Only visible pages remain in memory
  • Responsive UI: No freezing during PDF navigation

Optimized Performance

Memory consumption remains stable even with large PDF documents.

Memory Optimization

Conclusion

Displaying PDF files in Jetpack Compose requires careful consideration of performance and memory management. By implementing asynchronous rendering with coroutines, using StateFlow for reactive updates, and optimizing page loading based on visibility, you can create a smooth PDF viewing experience even with large documents. The key strategies include:

  1. Lazy loading: Render pages only when they become visible
  2. Memory management: Properly recycle bitmaps when pages leave the screen
  3. Asynchronous operations: Use coroutines to prevent UI freezing
  4. Dimension pre-calculation: Determine page sizes before rendering to optimize layout

This approach ensures that your PDF viewer remains responsive and memory-efficient regardless of document size.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.