DocuCompose is a modern, declarative PDF & Document Viewer library built for Compose Multiplatform (CMP) targeting Android and iOS (iosX64, iosArm64, iosSimulatorArm64).
It provides an intuitive @Composable interface that wraps native document rendering engines on both platformsβdelivering high performance, smooth scrolling, interactive pinch-to-zoom, and state observation with zero boilerplate.
- π± Native Performance:
- iOS: Uses Apple's native
PDFKit(PDFView&PDFDocument) viaUIKitViewinterop for smooth 60/120fps scrolling and pinch-to-zoom. - Android: Uses Android's native
android.graphics.pdf.PdfRendererwith LazyColumn virtualized rendering and transformable gestures.
- iOS: Uses Apple's native
- π Flexible Document Sources: Support for local file paths (
FilePath), raw byte arrays (ByteArraySource), and remote URLs (RemoteUrl). - ποΈ State Management: Reactive
DocumentViewerStateproviding real-time tracking ofcurrentPage,pageCount,zoomLevel,isLoading, anderror. - π¨ Customizable Placeholders & Callbacks: Easily supply custom Jetpack/Compose loading indicators and handle document opening errors gracefully.
- π¦ Modular Architecture: Clean separation into
:core(Models & Utilities) and:viewer(Composable UI & Native Interop). - π Maven Central Ready: Configured with Vanniktech's
com.vanniktech.maven.publishplugin for seamless open-source distribution.
DocuCompose is structured into three clean modules:
graph TD
App[":sample (CMP App)"] --> Viewer[":viewer (Expect/Actual Composable)"]
Viewer --> Core[":core (State, Model & Utilities)"]
| Module | Description | Package |
|---|---|---|
:core |
Defines DocumentSource, DocumentViewerState, rememberDocumentViewerState(), and cache utilities. |
io.github.hariagusw.docucompose.core |
:viewer |
Contains expect/actual @Composable fun DocumentViewer interop implementations for Android & iOS. |
io.github.hariagusw.docucompose.viewer |
:sample |
Cross-platform Compose Multiplatform demo app demonstrating library capabilities. | io.github.hariagusw.docucompose.sample |
Add DocuCompose to your Compose Multiplatform project.
[versions]
docucompose = "1.0.0-SNAPSHOT"
[libraries]
docucompose-core = { module = "io.github.hariagusw.docucompose:docucompose-core", version.ref = "docucompose" }
docucompose-viewer = { module = "io.github.hariagusw.docucompose:docucompose-viewer", version.ref = "docucompose" }kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.docucompose.core)
implementation(libs.docucompose.viewer)
}
}
}import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.github.hariagusw.docucompose.core.DocumentSource
import io.github.hariagusw.docucompose.viewer.DocumentViewer
@Composable
fun SimplePdfScreen() {
DocumentViewer(
source = DocumentSource.RemoteUrl("https://example.com/sample.pdf"),
modifier = Modifier.fillMaxSize()
)
}Use rememberDocumentViewerState() to inspect progress, jump between pages, or read the current zoom level:
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.hariagusw.docucompose.core.DocumentSource
import io.github.hariagusw.docucompose.core.rememberDocumentViewerState
import io.github.hariagusw.docucompose.viewer.DocumentViewer
@Composable
fun ControlledDocumentScreen() {
val viewerState = rememberDocumentViewerState()
Column(modifier = Modifier.fillMaxSize()) {
// Document Viewer Area
Box(modifier = Modifier.weight(1f)) {
DocumentViewer(
source = DocumentSource.RemoteUrl("https://pdfobject.com/pdf/sample.pdf"),
state = viewerState,
enableZoom = true,
onError = { throwable ->
println("Failed to load PDF: ${throwable.message}")
},
loadingPlaceholder = {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
)
}
// Control Bar
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = { viewerState.jumpToPage(viewerState.currentPage - 1) },
enabled = viewerState.currentPage > 1
) {
Text("Previous")
}
Text(
text = "Page ${viewerState.currentPage} of ${viewerState.pageCount}",
style = MaterialTheme.typography.bodyMedium
)
Button(
onClick = { viewerState.jumpToPage(viewerState.currentPage + 1) },
enabled = viewerState.currentPage < viewerState.pageCount
) {
Text("Next")
}
}
}
}
}DocuCompose supports three primary document source types:
// 1. Remote HTTP/HTTPS URL
val remoteSource = DocumentSource.RemoteUrl("https://example.com/document.pdf")
// 2. Local File System Path
val fileSource = DocumentSource.FilePath("/path/to/local/file.pdf")
// 3. Raw Byte Array
val byteArray = fetchPdfBytes()
val byteSource = DocumentSource.ByteArraySource(bytes = byteArray, name = "report.pdf")@Composable
expect fun DocumentViewer(
source: DocumentSource,
modifier: Modifier = Modifier,
state: DocumentViewerState = rememberDocumentViewerState(),
enableZoom: Boolean = true,
onError: (Throwable) -> Unit = {},
loadingPlaceholder: @Composable (() -> Unit)? = null
)| Parameter | Type | Default | Description |
|---|---|---|---|
source |
DocumentSource |
Required | Document content provider (RemoteUrl, FilePath, or ByteArraySource). |
modifier |
Modifier |
Modifier |
Composable modifier for sizing and styling layout. |
state |
DocumentViewerState |
rememberDocumentViewerState() |
State object for tracking document details and controlling page navigation. |
enableZoom |
Boolean |
true |
Toggles interactive pinch-to-zoom capability. |
onError |
(Throwable) -> Unit |
{} |
Callback invoked when an error occurs during PDF parsing or downloading. |
loadingPlaceholder |
@Composable (() -> Unit)? |
null |
Optional custom UI component shown while the document is loading. |
| Property / Method | Description |
|---|---|
currentPage: Int |
Current 1-based active page index. |
pageCount: Int |
Total count of pages in the open document. |
zoomLevel: Float |
Current zoom scale level (e.g. 1.0f = 100%). |
isLoading: Boolean |
true while document data is being fetched or prepared. |
error: Throwable? |
Holds any error thrown during loading. |
jumpToPage(page: Int) |
Navigates directly to the specified page index. |
updateZoom(zoom: Float) |
Adjusts zoom level within bounds [0.5f, 5.0f]. |
- JDK 17+
- Android SDK (installed at
~/Library/Android/sdkor specified vialocal.properties) - Xcode 16+ (for iOS builds on macOS)
./gradlew assemble./gradlew :sample:installDebugDocuCompose uses Vanniktech's com.vanniktech.maven.publish plugin. To publish to Maven Central, configure your credentials in ~/.gradle/gradle.properties:
mavenCentralUsername=YOUR_SONATYPE_USERNAME
mavenCentralPassword=YOUR_SONATYPE_PASSWORD
signing.keyId=YOUR_GPG_KEY_ID
signing.password=YOUR_GPG_PASSWORD
signing.secretKeyRingFile=/path/to/secring.gpgRun publish command:
./gradlew publishAndReleaseToMavenCentralCopyright 2026 Hari Agus W
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
