Tabbed Documents (Compose)
Render a floating-pill main toolbar with a rounded, spaced NutrientTabBar below it above a DocumentView, and open more documents into it.
/* * Copyright © 2026 PSPDFKit GmbH. All rights reserved. * * The Nutrient 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
import android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport androidx.activity.compose.BackHandlerimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.EnterTransitionimport androidx.compose.animation.ExitTransitionimport androidx.compose.animation.fadeInimport androidx.compose.animation.fadeOutimport androidx.compose.animation.slideInVerticallyimport androidx.compose.animation.slideOutVerticallyimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.WindowInsetsimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.imeimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.statusBarsPaddingimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Addimport androidx.compose.material3.Iconimport androidx.compose.material3.IconButtonimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.SmallFloatingActionButtonimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.layout.onSizeChangedimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.platform.LocalDensityimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.tasks.ExtractAssetTaskimport com.pspdfkit.catalog.ui.theming.CatalogThemeimport com.pspdfkit.catalog.ui.theming.enableConfigurationEdgeToEdgeimport com.pspdfkit.catalog.ui.theming.isNightThemeimport com.pspdfkit.compose.tabs.composables.NutrientTabBarimport com.pspdfkit.compose.tabs.composables.NutrientTabBarDefaultsimport com.pspdfkit.compose.theme.MainToolbarColorsimport com.pspdfkit.compose.theme.NutrientThemeimport com.pspdfkit.compose.theme.ToolbarPopupColorsimport com.pspdfkit.compose.theme.UiColorSchemeimport com.pspdfkit.compose.theme.getUiColorsimport com.pspdfkit.compose.toolbar.composables.NutrientMainToolbarimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.activity.TabBarHidingModeimport com.pspdfkit.jetpack.compose.interactors.rememberDocumentStateimport com.pspdfkit.jetpack.compose.views.DocumentViewimport com.pspdfkit.ui.DocumentCoordinatorimport com.pspdfkit.ui.DocumentDescriptorimport com.pspdfkit.ui.tabs.PdfTabBarCloseModeimport com.pspdfkit.utils.getSupportParcelableExtraimport io.nutrient.ui.theme.getTabBarColors
/** * Shows several documents in one [DocumentView] under app-owned chrome that floats over the document: a * [NutrientMainToolbar] rendered as a pill, and below it a [NutrientTabBar] with rounded, spaced tabs in the app's * Material colors plus an add button that opens another document in a new tab. Because the chrome overlays the * document rather than sitting above it, hiding it in immersive mode leaves the page where it is. * * Both bars bind to coordinators the [com.pspdfkit.jetpack.compose.interactors.DocumentState] publishes once the * document UI is ready: the toolbar coordinator and the [DocumentCoordinator]. The SDK's own main toolbar and tab * bar are disabled through the configuration. */class DocumentTabsComposeExample(context: Context) : SdkExample(context, R.string.documentTabsComposeExampleTitle, R.string.documentTabsComposeExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { ExtractAssetTask.extract(WELCOME_DOC, title, context) { documentFile -> val intent = Intent(context, DocumentTabsComposeActivity::class.java) intent.putExtra(DocumentTabsComposeActivity.EXTRA_URI, Uri.fromFile(documentFile)) context.startActivity(intent) } }}
class DocumentTabsComposeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
val uri = intent.getSupportParcelableExtra(EXTRA_URI, Uri::class.java)!! val configuration = PdfActivityConfiguration.Builder(this) // The activity renders its own main toolbar and tab bar above the document, so the SDK's stay hidden. .defaultToolbarEnabled(false) .setTabBarHidingMode(TabBarHidingMode.HIDE) // Keep the pill to a single row: no document title overlay strip below the toolbar items. .documentTitleOverlayEnabled(false) .build()
enableConfigurationEdgeToEdge(configuration) setContent { CatalogTheme(darkTheme = configuration.isNightTheme) { TabbedDocuments(uri, configuration) } } }
companion object { const val EXTRA_URI = "DocumentTabsComposeActivity.DocumentUri" }}
@Composableprivate fun TabbedDocuments(uri: Uri, configuration: PdfActivityConfiguration) { val context = LocalContext.current val documentState = rememberDocumentState(uri, configuration)
// The DocumentView publishes both coordinators once its UI is ready. They are null until then, so the bars are // rendered only once they are non-null. val toolbarCoordinator by documentState.toolbarCoordinator.collectAsState() val documentCoordinator by documentState.documentCoordinator.collectAsState()
// Let the SDK handle back the layered way: close an overlay view (outline, page grid, ...) first. val overlayViewShown by documentState.viewWithOverlappingToolbarShown.collectAsState() val imeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 BackHandler(enabled = overlayViewShown && !imeVisible) { documentState.handleBackPress() }
// Cycles through the extra documents the add button opens. var nextDocument by rememberSaveable { mutableIntStateOf(0) }
// The toolbar frame's visibility flips with immersive mode (tap-to-hide chrome). The SDK's own toolbar animates // on it; here the whole chrome, pill and tab bar, follows it so that both leave together. val chromeVisible = toolbarCoordinator?.let { it.frame.collectAsState().value.isVisible } ?: true
// Overlay views (outline, page grid, ...) are laid out by the SDK inside the DocumentView, which the chrome // covers. Push their content below the chrome while one is shown; the document itself stays put. var chromeHeightPx by remember { mutableStateOf(0) } LaunchedEffect(overlayViewShown, chromeHeightPx) { documentState.setContentViewTopPadding(if (overlayViewShown) chromeHeightPx else 0) }
val chromeColor = MaterialTheme.colorScheme.surfaceContainer Box(Modifier.fillMaxSize()) { // The document fills the screen and sits behind the chrome, so showing or hiding the chrome never moves it. DocumentView( documentState = documentState, modifier = Modifier.fillMaxSize(), ) AnimatedVisibility( visible = chromeVisible, enter = slideInVertically { -it } + fadeIn(), exit = slideOutVertically { -it } + fadeOut(), modifier = Modifier.align(Alignment.TopCenter).background(Color.Transparent), ) { Column( Modifier .fillMaxWidth() .background(Color.Transparent) .statusBarsPadding() .onSizeChanged { chromeHeightPx = it.height }, ) { toolbarCoordinator?.let { coordinator -> // The floating, rounded, elevated main toolbar. NutrientTheme provides the pill's color scheme // to the toolbar items, the title and the overflow popup. NutrientTheme(colorScheme = pillToolbarColors()) { Surface( shape = RoundedCornerShape(PILL_CORNER_RADIUS), shadowElevation = 8.dp, color = Color.Transparent, modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 8.dp) .clip(RoundedCornerShape(PILL_CORNER_RADIUS)), ) { NutrientMainToolbar( coordinator = coordinator, // The chrome already offsets the status bar, so the toolbar must not add its own inset. windowInsets = WindowInsets(0, 0, 0, 0), // The chrome animates as a whole, so the toolbar must not animate on its own too. enter = EnterTransition.None, exit = ExitTransition.None, ) } } } documentCoordinator?.let { coordinator -> Row(verticalAlignment = Alignment.CenterVertically) { NutrientTabBar( documentCoordinator = coordinator, modifier = Modifier.weight(1f).padding(top = 5.dp), closeMode = PdfTabBarCloseMode.CLOSE_ONLY_SELECTED_TAB, colors = getTabBarColors( backgroundColor = Color.Transparent, tabColor = MaterialTheme.colorScheme.surface, selectedTabColor = MaterialTheme.colorScheme.primary, selectedTextColor = MaterialTheme.colorScheme.onPrimary, ), dimensions = NutrientTabBarDefaults.dimensions(height = 52.dp, tabMinWidth = 96.dp), tabShape = RoundedCornerShape(PILL_CORNER_RADIUS), tabSpacing = 8.dp, contentPadding = PaddingValues(start = 12.dp, top = 4.dp, end = 4.dp, bottom = 8.dp), ) SmallFloatingActionButton({ val assetName = MORE_DOCUMENTS[nextDocument % MORE_DOCUMENTS.size] nextDocument++ ExtractAssetTask.extract(assetName, assetName, context) { documentFile -> openInNewTab(coordinator, Uri.fromFile(documentFile)) } }) { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.documentTabsComposeExampleAddTab)) } } } } } }}
/** The pill's color scheme: the app's primary color with white content, for items, title and the overflow popup. */@Composableprivate fun pillToolbarColors(): UiColorScheme { val background = MaterialTheme.colorScheme.primary return getUiColors().copy( mainToolbar = MainToolbarColors( backgroundColor = background, textColor = Color.White, popup = ToolbarPopupColors(backgroundColor = background), titleTextColor = Color.White, ), )}
/** Adds the document at [uri] to [coordinator] and switches to it, which selects its tab. */private fun openInNewTab(coordinator: DocumentCoordinator, uri: Uri) { val descriptor = DocumentDescriptor.fromUri(uri) coordinator.addDocument(descriptor) coordinator.setVisibleDocument(descriptor)}
private val MORE_DOCUMENTS = listOf("Classbook.pdf", "Aviation.pdf", "Annotations.pdf")private val PILL_CORNER_RADIUS = 28.dpThis code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.