> ## Documentation Index
> Fetch the complete documentation index at: https://mahmoud-b28887f9.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshoot KPDF: common integration issues and fixes

> Diagnose and fix the most common KPDF problems: idle state flows, offline caching, slow thumbnails, zoom limits, swipe navigation, and export failures.

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.

<AccordionGroup>
  <Accordion title="openDocumentState stays at Idle">
    **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.

    ```kotlin theme={null}
    @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 `AwaitingSelection` → `Success` (or `Cancelled` / `Error`) without resetting mid-flight.
  </Accordion>

  <Accordion title="PDF loads from URL but fails offline">
    **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.

    ```kotlin theme={null}
    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`.
  </Accordion>

  <Accordion title="Thumbnails are slow or blank">
    **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.

    ```kotlin theme={null}
    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.
  </Accordion>

  <Accordion title="Save flow does nothing / stays at Idle">
    **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.

    ```kotlin theme={null}
    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.
  </Accordion>

  <Accordion title="externalOpenState stays at Idle">
    **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`.

    ```kotlin theme={null}
    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.")
    }
    ```
  </Accordion>

  <Accordion title="Zoom controls are disabled">
    **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.

    ```kotlin theme={null}
    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.

    ```kotlin theme={null}
    val zoom by viewerState.currentZoom.collectAsState()
    ```
  </Accordion>

  <Accordion title="Swipe navigation doesn't work">
    **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.

    ```kotlin theme={null}
    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.
  </Accordion>

  <Accordion title="exportPdf() returns a failure">
    **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.

    ```kotlin theme={null}
    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.
  </Accordion>
</AccordionGroup>

<Note>
  If your issue isn't covered here, open an issue or search existing discussions at [github.com/mahmoud947/KPDF](https://github.com/mahmoud947/KPDF).
</Note>
