Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mahmoud-b28887f9.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

This page covers the most common problems you may encounter when integrating KPDF into a Compose Multiplatform application. For each issue you’ll find the root cause and a concrete fix, with code examples where they help.
Cause: KPdfViewerState is being recreated on every recomposition because you are building KPdfViewerConfig inline — without remember. Each new config instance causes rememberPdfViewerState to return a brand-new state object, which resets transient flows like openDocumentState before they can deliver a result.Fix: Wrap both source and config in remember so they remain stable across recompositions.
@Composable
fun PdfScreen(source: KPdfSource) {
    // Stable source — only changes when the incoming value changes
    val stableSource = remember(source) { source }

    // Config built once and reused
    val viewerConfig = remember {
        KPdfViewerConfig.builder()
            .enableSwipe(true)
            .diskCacheSize(50)
            .preloadPageCount(1)
            .build()
    }

    val viewerState = rememberPdfViewerState(
        source = stableSource,
        config = viewerConfig,
    )

    val openState by viewerState.openDocumentState.collectAsState()

    LaunchedEffect(openState) {
        val selectedSource = (openState as? KPdfOpenDocumentState.Success)?.source
            ?: return@LaunchedEffect
        viewerState.open(selectedSource)
    }

    KPdfViewer(state = viewerState)
}
After this change, openDocumentState will progress through AwaitingSelectionSuccess (or Cancelled / Error) without resetting mid-flight.
Cause: KPdfSource.Url downloads and renders the PDF over the network. By default, KPDF can cache the downloaded bytes to disk so that subsequent opens work without a network connection.If diskCacheSize is 0, disk caching is disabled, and the document will fail to open when the device is offline.Fix: Set a non-zero diskCacheSize in your config. The value is the maximum number of rendered pages to persist to disk.
val viewerConfig = remember {
    KPdfViewerConfig.builder()
        .diskCacheSize(50) // cache up to 50 rendered pages on disk
        .build()
}
Once the PDF has been downloaded at least once with a non-zero cache size, KPDF will serve it from the disk cache on subsequent opens — even without a network connection.If you need to keep storage use minimal, set diskCacheSize to a small positive value (for example 10) rather than 0.
Cause: KPDF renders thumbnails asynchronously via renderPage(). Each thumbnail is a separate render call that runs off the main thread. On low-end devices, or when thumbnailWidth/thumbnailHeight are set too large, render times can be long enough that thumbnails appear blank for several seconds.Fix: Keep thumbnail dimensions reasonable. Values around 92.dp × 128.dp give a good balance between visual quality and render speed.
KPdfThumbnailStrip(
    state = viewerState,
    thumbnailWidth = 92.dp,
    thumbnailHeight = 128.dp,
    onPageClick = { pageIndex -> viewerState.goToPage(pageIndex) },
)
If a thumbnail enters an error state (for example because a render call fails), you can tap it to retry. KPDF will call renderPage() again for that page index.Reducing thumbnailWidth and thumbnailHeight is the single most effective way to speed up the strip on constrained hardware.
Cause: The same root cause as the openDocumentState issue: KPdfViewerState is being recreated because KPdfViewerConfig is rebuilt inline on every recomposition. When the state resets, the saveState flow reverts to Idle before the platform save dialog can return a result.Fix: Wrap source and config in remember so the viewer state is stable.
val viewerConfig = remember {
    KPdfViewerConfig.builder()
        .diskCacheSize(50)
        .build()
}

val viewerState = rememberPdfViewerState(
    source = stableSource,
    config = viewerConfig,
)

val saveState by viewerState.saveState.collectAsState()

Button(onClick = { viewerState.requestSave() }) {
    Text("Save")
}

when (saveState) {
    KPdfSaveState.Idle -> Unit
    KPdfSaveState.Exporting -> Text("Preparing PDF...")
    is KPdfSaveState.AwaitingDestination -> Text("Choose where to save the file.")
    is KPdfSaveState.Success -> Text("PDF saved.")
    is KPdfSaveState.Cancelled -> Text("Save cancelled.")
    is KPdfSaveState.Error -> Text("Save failed.")
}
With a stable viewer state, saveState will progress through the full lifecycle instead of silently resetting.
Cause: Same root cause as the save and open-document issues: a newly constructed KPdfViewerConfig on each recomposition recreates the viewer state, which resets externalOpenState to Idle before the platform flow completes.Fix: Stabilize source and config with remember.
val viewerConfig = remember {
    KPdfViewerConfig.builder()
        .build()
}

val viewerState = rememberPdfViewerState(
    source = stableSource,
    config = viewerConfig,
)

val externalOpenState by viewerState.externalOpenState.collectAsState()

Button(onClick = { viewerState.openInExternalApp() }) {
    Text("Open In External App")
}

when (externalOpenState) {
    KPdfExternalOpenState.Idle -> Unit
    KPdfExternalOpenState.Exporting -> Text("Preparing PDF...")
    is KPdfExternalOpenState.AwaitingExternalApp -> Text("Opening external app...")
    is KPdfExternalOpenState.Success -> Text("External app opened.")
    is KPdfExternalOpenState.Cancelled -> Text("Open was cancelled.")
    is KPdfExternalOpenState.Error -> Text("Unable to open external app.")
}
Cause: The zoomIn() and zoomOut() actions disable themselves when currentZoom reaches the bounds defined by zoomRange() in your config. If zoom is also disabled entirely via enableZoom(false), both controls will be inactive regardless of the current zoom level.Fix: Check two things in your config:
  1. Call enableZoom(true) to enable zoom interaction.
  2. Call zoomRange(minZoom, maxZoom) with values that give the user room to zoom. If minZoom equals maxZoom, neither button will ever be enabled.
val viewerConfig = remember {
    KPdfViewerConfig.builder()
        .enableZoom(true)
        .zoomRange(minZoom = 1f, maxZoom = 4f) // allows zooming from 1× to 4×
        .doubleTapZoom(2f)
        .build()
}
You can read currentZoom to understand the current zoom level at runtime and decide whether to render your own zoom controls in a disabled state.
val zoom by viewerState.currentZoom.collectAsState()
Cause: Swipe page navigation is disabled by default. Even when enabled, swiping only triggers at the base zoom level. If the user has zoomed in, swipe gestures pan the page instead of navigating to the next or previous page.Fix: Enable swipe in your config.
val viewerConfig = remember {
    KPdfViewerConfig.builder()
        .enableSwipe(true)
        .build()
}
If swipe still doesn’t advance pages, check currentZoom. Swipe-to-navigate only activates when the viewer is at the minimum zoom level (base zoom). Ask the user to reset zoom first, or call viewerState.resetZoom() programmatically before expecting swipe navigation to work.
Cause: exportPdf() operates on the currently loaded source. If you call it before the document has finished loading — that is, before loadState reaches Ready — it will return a failure because there is no loaded content to export.Fix: Observe loadState and only call exportPdf() once it signals that the document is ready.
val scope = rememberCoroutineScope()
val loadState by viewerState.loadState.collectAsState()

Button(
    enabled = loadState is KPdfLoadState.Ready,
    onClick = {
        scope.launch {
            viewerState.exportPdf().fold(
                onSuccess = { pdfBytes ->
                    sharePdfBytes(pdfBytes)
                },
                onFailure = { error ->
                    // Handle the error — log it or show a message to the user
                    println("Export failed: ${error.message}")
                }
            )
        }
    }
) {
    Text("Share")
}
Always handle the onFailure branch. Sources that fail to load, are cancelled mid-flight, or encounter a rendering error will all surface through the failure path.
If your issue isn’t covered here, open an issue or search existing discussions at github.com/mahmoud947/KPDF.