How to Show Pdf with Jetpack Compose

How to Show Pdf with Jetpack Compose

How to Show Pdf in Jetpack Compose without using any third party library?

What is PDF?

PDF means Portable Document Format developed by Abode in the year 1992. It is a combination of vector and bitmap graphics. It is popular as it is portable and less memory consuming format and the most advance feature is that it can be encrypted for security in which a password is required to open, edit or view the contents.

 How to Show Pdf in Jetpack Compose?

There are many third party external library are available to display PDF files. To display a PDF in Jetpack Compose, you need to use an external library because Jetpack Compose doesn't have built-in support for PDF rendering. One popular library for this purpose is AndroidPdfViewer.

dependencies {
implementation ("com.github.barteksc:android-pdf-viewer:3.2.0-beta.1")
}

But in this tutorial, we are going to use the built-in Android APIs. Specifically, you can use Pdf Renderer to render PDF pages into bitmaps, which can then be displayed using Jetpack Compose's Image composable. 

Why Pdf Renderer is used?

Pdf Renderer is used to render each page of the PDF into a bitmap, which is then displayed using Jetpack Compose's Image composable. So, this approach does not rely on any third-party libraries and uses only Android's built-in PDF rendering capabilities.

Steps:

1. Open Android Studio
2. Create a New Project and choose Empty Compose Activity
3. Create assets folder and save pdf files in it.
4. MainAcitivity
5. Create a new file named PdfViewerApp

Create an assets folder

Create an assets folder

Create an assets folder

MainActivity

Copy this code →

package com.codingbihar.pdfviewerapp

import ...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            JetpackComposeSkillTheme {
             val navController = rememberNavController()
                PdfViewerApp(navController)

            }
        }
    }
}

PdfViewerApp

Copy this code →

package com.codingbihar.pdfviewerapp

import ...

@Composable
fun PdfViewerApp(navController: NavHostController) {
    NavHost(navController, startDestination = "list") {
        composable("list") { PdfList(navController) }
        composable("pdfViewer/{fileName}") { backStackEntry ->
            PdfViewer(
                navController,
                fileName = backStackEntry.arguments?.getString("fileName") ?: ""
            )
        }
    }
}
@Composable
fun PdfList(navController: NavHostController) {
    val context = LocalContext.current
    val pdfFiles = remember { mutableStateOf>(emptyList()) }

    LaunchedEffect(Unit) {
        pdfFiles.value = try {
            context.assets.list("")?.filter { it.endsWith(".pdf") } ?: emptyList()
        } catch (e: IOException) {
            emptyList()
        }
    }

    Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
        pdfFiles.value.forEach { fileName ->
            Button(onClick = {
                navController.navigate("pdfViewer/$fileName")
            }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
                Text(text = fileName)
            }
        }
        if (pdfFiles.value.isEmpty()) {
            Text("No PDFs available", modifier = Modifier.fillMaxWidth().padding(top = 16.dp))
        }
    }
}

@Composable
fun PdfViewer(navController: NavHostController, fileName: String) {
    val context = LocalContext.current
    val zoomState = remember { mutableFloatStateOf(1f) }
    val bitmapState = remember { mutableStateOf(null) }
    val coroutineScope = rememberCoroutineScope()
    val currentPage = remember { mutableIntStateOf(0) }
    val totalPages = remember { mutableIntStateOf(0) }

    // Handle back press to navigate back to the list
    BackHandler {
        navController.popBackStack()
    }

    LaunchedEffect(fileName, currentPage.intValue) {
        coroutineScope.launch {
            try {
                // Copy PDF from assets to cache
                val cacheFile = File(context.cacheDir, fileName)
                if (!cacheFile.exists()) {
                    context.assets.open(fileName).use { inputStream ->
                        FileOutputStream(cacheFile).use { outputStream ->
                            inputStream.copyTo(outputStream)
                        }
                    }
                }

                // Open PDF file and get the current page
                val fileDescriptor = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.MODE_READ_ONLY)
                val pdfRenderer = PdfRenderer(fileDescriptor)
                totalPages.intValue = pdfRenderer.pageCount // Set total pages

                val page = pdfRenderer.openPage(currentPage.intValue)
                val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
                page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)

                withContext(Dispatchers.Main) {
                    bitmapState.value = bitmap
                }
                page.close()
                pdfRenderer.close()
                fileDescriptor.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        Box(modifier = Modifier
            .weight(1f)
            .fillMaxSize()
            .pointerInput(Unit) {
                detectHorizontalDragGestures { change, dragAmount ->
                    change.consume()
                    if (dragAmount > 0) {
                        // Swipe right to left
                        if (currentPage.intValue < totalPages.intValue - 1) {
                            currentPage.intValue += 1
                        }
                    } else if (dragAmount < 0) {
                        // Swipe left to right
                        if (currentPage.intValue > 0) {
                            currentPage.intValue -= 1
                        }
                    }
                }
            }) {
            bitmapState.value?.let { bmp ->
                Image(
                    bitmap = bmp.asImageBitmap(),
                    contentDescription = "PDF Page",
                    modifier = Modifier
                        .fillMaxSize()
                        .simpleZoomable(zoomState)
                        .graphicsLayer(
                            scaleX = zoomState.floatValue,
                            scaleY = zoomState.floatValue
                        ),
                    contentScale = ContentScale.Fit
                )
            } ?: run {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
            }
        }

        // Navigation Controls (Optional, for testing purposes)
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(
                onClick = {
                    if (currentPage.intValue > 0) {
                        currentPage.intValue -= 1
                    }
                }
            ) {
                Text("Previous")
            }
            Button(
                onClick = {
                    if (currentPage.intValue < totalPages.intValue - 1) {
                        currentPage.intValue += 1
                    }
                }
            ) {
                Text("Next")
            }
        }
    }
}

fun Modifier.simpleZoomable(
    zoomState: MutableState
) = this.pointerInput(Unit) {
    detectTransformGestures { _, _, zoomChange, _ ->
        val newZoom = max(1f, zoomState.value * zoomChange)
        zoomState.value = min(2f, newZoom)

    }
}

OUTPUT:

Pdf Viewer App Output 1
Previous Post Next Post