Compose PDF Viewer
A complete Compose viewer app: recent documents with page thumbnails, system file import, light/dark/dynamic-color settings, and a host-rendered main toolbar.
/* * Copyright © 2026 PSPDFKit GmbH. All rights reserved. * * The PSPDFKit Sample applications are licensed with a modified BSD license. * Please see License for details. This notice may not be removed from this file. */
package com.pspdfkit.catalog.examples.kotlin.composeviewer
import android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.foundation.isSystemInDarkThemeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.getValueimport androidx.lifecycle.viewmodel.compose.viewModelimport androidx.navigation.compose.NavHostimport androidx.navigation.compose.composableimport androidx.navigation.compose.rememberNavControllerimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.tasks.ExtractAssetTaskimport com.pspdfkit.catalog.ui.theming.CatalogThemeimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.utils.getSupportParcelableExtraimport java.io.File
/** * A complete Jetpack Compose PDF viewer app in one catalog example — the in-catalog successor * of the standalone `simple-compose` app: * * - **Library**: recent documents as a grid of live page-1 thumbnails (rendered with * `PdfDocumentLoader`), with system file import (SAF). * - **Viewer** ([ViewerScreen]): `DocumentView` with a host-rendered `NutrientMainToolbar`, * the content/viewport inset bookkeeping a custom main toolbar requires, custom toolbar * items, `DefaultListeners` wiring, and a live demonstration of * [com.pspdfkit.jetpack.compose.interactors.DocumentState.setCustomPdfSource]'s async * completion and view-state restore. * - **Settings**: light/dark/system theme and Material You dynamic color, persisted in a * DataStore; the choice drives both the app chrome and the document theme. * * State lives in [ComposeViewerViewModel] — plain `ViewModel` + DataStore, no DI framework or * database, so the whole example is copy-paste-able. */class ComposePdfViewerExample(context: Context) : SdkExample(context, R.string.composePdfViewerExampleTitle, R.string.composePdfViewerExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { // Extract the bundled demo document so a fresh install starts with a library entry. ExtractAssetTask.extract(WELCOME_DOC, title, context) { documentFile -> val intent = Intent(context, ComposePdfViewerActivity::class.java) .putExtra(ComposePdfViewerActivity.EXTRA_DEMO_DOCUMENT, Uri.fromFile(documentFile)) context.startActivity(intent) } }}
class ComposePdfViewerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) val demoDocument = intent.getSupportParcelableExtra(EXTRA_DEMO_DOCUMENT, Uri::class.java)
setContent { val viewModel: ComposeViewerViewModel = viewModel() // Seed the library with the bundled demo document on every launch — recordOpened // upserts, so an existing entry only refreshes its timestamp. Label it with the asset // name: ExtractAssetTask writes the file as "<asset>_<sha1-of-title>", which is not a // name to show a reader. LaunchedEffect(viewModel) { demoDocument?.path?.let { viewModel.recordOpened(File(it), name = SdkExample.WELCOME_DOC) } }
val darkMode by viewModel.darkMode.collectAsState() val dynamicColor by viewModel.dynamicColor.collectAsState() // The catalog's own theme (brand palette + typography), driven by this example's // persisted settings rather than CatalogTheme's system defaults — so the Settings // screen's theme and dynamic-color controls still work while the viewer looks like // the rest of the catalog. CatalogTheme(darkTheme = viewerDarkMode(darkMode), dynamicColor = dynamicColor) { ViewerNavHost(viewModel) } } }
companion object { const val EXTRA_DEMO_DOCUMENT = "ComposePdfViewerActivity.DemoDocument" }}
/** The app's three destinations; the viewer takes the document's file path as its argument. */@Composableprivate fun ViewerNavHost(viewModel: ComposeViewerViewModel) { val navController = rememberNavController() NavHost(navController = navController, startDestination = "library") { composable("library") { LibraryScreen( viewModel = viewModel, onOpenDocument = { document -> viewModel.recordOpened(File(document.path)) navController.navigate("viewer/${Uri.encode(document.path)}") }, onOpenSettings = { navController.navigate("settings") }, ) } composable("settings") { SettingsScreen(viewModel = viewModel, onBack = { navController.popBackStack() }) } composable("viewer/{path}") { backStackEntry -> val path = Uri.decode(backStackEntry.arguments?.getString("path").orEmpty()) ViewerScreen( path = path, isDark = viewerInDarkTheme(viewModel), onBack = { navController.popBackStack() }, ) } }}
/** Resolves the effective dark flag from the tri-state setting. */@Composableinternal fun viewerInDarkTheme(viewModel: ComposeViewerViewModel): Boolean { val darkMode by viewModel.darkMode.collectAsState() return viewerDarkMode(darkMode)}
/** Resolves the tri-state [ViewerDarkMode] setting into the effective dark flag. */@Composableprivate fun viewerDarkMode(darkMode: Int): Boolean = when (darkMode) { ViewerDarkMode.DARK -> true ViewerDarkMode.LIGHT -> false else -> isSystemInDarkTheme()}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.