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
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)
}
}