This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/android/releases/12-1.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Nutrient Android SDK 12.1 release notes

Nutrient Android SDK 12.1 release notes

RSS

10 November 2026

Nutrient Android SDK 12.1 replaces the SDK’s RxJava APIs with Kotlin coroutines and Flow. It also removes RxJava from the dependencies the SDK brings into your app. You can now theme every SDK screen from code with one call, including separate light and dark color schemes built from your Material 3 theme. Electronic signatures gain configurable stroke thickness and opacity, and a new interceptor enables you to step into the signing flow. This release fixes corrupted documents when saving over a larger file on Android 10 and later, plus many rendering, redaction, and Content Editor issues.

Because the RxJava APIs are removed without a deprecation period, this release contains source-breaking API changes. The migration guide covers every change you need to react to, with before and after code. For the full list of changes, see our changelog.

Kotlin coroutines instead of RxJava

Every SDK method that returned a Single, Maybe, Completable, Observable, or Flowable is now a suspending function or returns a Kotlin Flow. Loading a document, saving it, rendering a page, searching, or reading form fields is now a plain call inside a coroutine. That work is canceled with the scope that launched it, so you no longer need to manage disposables for it:

lifecycleScope.launch {
val document = PdfDocumentLoader.openDocumentAsync(context, source)
val fields = document.formProvider.getFormFields()
TextSearch(document, configuration).performSearchAsync(query).collect { result -> show(result) }
}

The Instant APIs follow the same model. InstantClient.openDocumentAsync and InstantPdfDocument.reauthenticateWithJwtAsync are suspending functions, and document downloads and annotation syncs report their progress as a Flow of InstantProgress.

The SDK no longer brings RxJava into your app, so apps that don’t use RxJava get a smaller dependency graph. kotlinx-coroutines-core is now an API dependency, so the Flow and Job types the SDK returns are always available. If your codebase is built around RxJava, you don’t have to convert it all at once. Wrap SDK calls with the kotlinx-coroutines-rx3 adapters and keep your existing subscriptions. The Catalog’s RxJava interop example shows how.

Theme every SDK screen from code

PdfUi.setColorScheme(), available on PdfActivity and PdfUiFragment, themes the SDK’s user interface from a Compose UiColorScheme instead of XML theme attributes. One call covers the main toolbar, the outline view (with its bookmark, annotation, and attachment lists), and document info. It also covers the document tab bar, the share menu, the settings sheet, and AI Assistant. The annotation creation toolbar, the thumbnail bar, and the thumbnail grid follow the scheme too, through the new contextualToolbar, thumbnailBar, and thumbnailGrid properties of UiColorScheme.

Pass two schemes to follow light and dark mode. setColorScheme(light, dark) takes either two UiColorSchemes or two Material 3 ColorSchemes. It follows the activity’s theme, including dark mode under a DayNight theme, so you don’t need to reset the scheme after every configuration change:

setColorScheme(
lightColorScheme(primary = brandColor),
darkColorScheme(primary = brandColorDark),
)

Building a scheme takes less code:

  • UiColorScheme.fromMaterial() builds a complete scheme from your app’s Material 3 ColorScheme. Any color you leave unspecified takes its matching Material role, so you only override what you want to change.
  • UiColorScheme.fromXmlTheme(context) reads your existing XML theme outside of composition as a starting point for code-based theming.
  • UiColorScheme.withAccent(accent, onAccent) applies one brand accent to the selection colors of every SDK screen.

NutrientTheme now picks up the scheme set with setColorScheme(), so your own Compose content matches the SDK screens around it. It also applies the scheme as a MaterialTheme, so stock Material 3 components in SDK screens follow the SDK theme in light and dark mode. Sheet handles, switches, ripples, and text fields no longer use Compose’s default light palette.

Separately, the defaults of UiColorScheme.default() were reworked for legibility. For example, main toolbar text no longer matches its background, and swiping to delete in the outline uses the error color.

For hosts that lay out their own floating toolbars, NutrientFloatingToolbarMenu shows the same overflow menu as NutrientFloatingToolbar. That menu now uses the Material 3 Expressive style. Text selection handles are also easier to see: They now stand out from the highlighted text while dragging and on light and inverted pages.

Signature thickness, opacity, and placement

Electronic signatures no longer have to look the same in every app. ElectronicSignatureOptions gains defaultThickness and defaultAlpha, which set the stroke thickness and opacity of signatures created with the built-in signature dialog. The new Signature.alpha is applied to the annotation created from a signature. The annotation configuration registry has signature-specific defaults for AnnotationTool.SIGNATURE that take precedence over the ink defaults.

PdfFragment.signatureCreationInterceptor enables you to step into the built-in flow without replacing its user interface. The SDK calls it before showing the signature dialog, where you can adjust the options or cancel. It calls it again before adding a created signature to the document, where you can choose a different page and bounding box. You can resolve each request immediately or after showing your own UI, such as a thickness and opacity picker:

pdfFragment.signatureCreationInterceptor = object : SignatureCreationInterceptor {
override fun onSignatureCreationRequested(request: SignatureCreationRequest) {
request.proceed(request.options.copy(defaultThickness = 2f, defaultAlpha = 0.8f))
}
}

ElectronicSignatureFragment no longer loses its confirm button when saved signatures load slowly. Signatures created after a rotation are now saved to the SignatureStorage when the saving strategy asks for it.

Rendering and zoom

  • Deeper zoom — The default maximum zoom scale rises from 40 to 50. Pages no longer briefly flash blank or show the wrong area while zooming or panning at very high zoom levels. A light pinch or Control-mouse wheel zoom in continuous scrolling no longer jumps to another page.
  • Transparency — Pages with many nested transparency groups render faster and show the correct colors at their edges. They no longer fail to finish rendering. Pages that use certain blend modes render correctly while they load progressively.
  • Images — 8-bit lossless JPEG images, which previously didn’t display, are now supported. Very large JPEG images are now drawn at a lower resolution instead of being left out of the page.

Content Editor, annotations, and redaction

  • Content Editor — Text renders faster while editing. Changing the font, bold, or italic style no longer runs words together or drops letter spacing. Text and images no longer disappear or change font after saving.
  • Annotations and forms — Multicharacter emojis, such as family and flag emojis, stay intact. Nested bold and italic rich text renders both styles. Non-ASCII author and subject names survive saving. Shrink-to-fit text annotations and form fields export at the right size. Ink strokes are no longer clipped. Flattening a document whose annotation appearance can’t be generated works again after a regression in 12.0.0.
  • Redaction — Redacted form field values are no longer recoverable in documents with nested form fields. Redactions over images and large JPEG scans apply correctly. Saving with DocumentSaveOptions.setApplyRedactions(true) no longer removes text from pages with nothing redacted on them.
  • Search and text extraction — Exact word matching in document library searches works for multiword queries and words at the start or end of a line. Text extraction returns lines in the correct order in accessible PDFs.

Saving and stability

Documents saved over a larger file on Android 10 and later are no longer corrupted. This affected, for example, signing into an existing file or saving a document with fewer pages. Apps that open many documents no longer see growing thread counts and memory usage, because the SDK releases each document’s background threads once it’s no longer used.

This release also fixes an Application Not Responding (ANR) error when leaving a document with autosave enabled while a page is still rendering. It fixes crashes when saving a document while its annotations are still loading and when rendering pages under memory pressure. It also fixes a crash when the container hosting a PdfFragment is briefly laid out with a negative size during an orientation change. For Instant, offline image annotations now upload their image after a failed sync instead of being resent indefinitely.

Every release now ships with a CycloneDX software bill of materials (SBOM) that lists the SDK’s third-party components and their versions. You can use it in your supply chain and license audits.


Migration guide

Version 12.1 replaces the RxJava API with Kotlin coroutines, removes RxJava from the SDK’s dependencies, and changes how color schemes set in code are resolved. It also deprecates several theming APIs, changes a few defaults, and contains small binary-breaking changes that require recompiling. Work through the sections that apply to your integration.

Migrating with a coding agent

To have a coding agent such as Claude Code, Codex, or Gemini CLI do the migration, give it this prompt. For better results, install the Nutrient Android SDK agent skill first. It includes the same migration steps:

Migrate this Android app to Nutrient Android SDK 12.1.
If it's on 11.x, first apply https://www.nutrient.io/guides/android/migration-guides/12-migration-guide.md.
Then apply the "Migration guide" section of https://www.nutrient.io/guides/android/releases/12-1.md.
Before editing, ask me whether to keep RxJava at the SDK boundary with kotlinx-coroutines-rx3 or to convert the affected code to coroutines.
If the app depends on io.nutrient:nutrient, move it to io.nutrient:nutrient-android-sdk on Maven Central.
Confirm each replacement against the 12.1 SDK, change only what the migration needs, build after each step, and list anything you couldn't map.

RxJava replaced by Kotlin coroutines

Every SDK method that returned a Single, Maybe, Completable, Observable, or Flowable is now a suspending function or returns a Flow. The previous signatures are removed without a deprecation period. Update your integration if you call one of these APIs, implement an affected interface, or rely on the SDK to supply RxJava dependencies.

You can either keep RxJava in your app and adapt the SDK calls at the boundary, or convert the affected code to coroutines. Both approaches start with your dependencies.

Update your dependencies

The SDK no longer declares io.reactivex.rxjava3:rxjava, io.reactivex.rxjava3:rxandroid, org.jetbrains.kotlinx:kotlinx-coroutines-rx3, or androidx.compose.runtime:runtime-rxjava3 as API dependencies. Declare the ones your app still uses, keeping the versions you resolve today:

dependencies {
// If your app uses RxJava:
implementation("io.reactivex.rxjava3:rxjava:<version>")
// If it uses `AndroidSchedulers` or other RxAndroid APIs:
implementation("io.reactivex.rxjava3:rxandroid:<version>")
// If it adapts between coroutines and RxJava:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx3:<version>")
// If it observes RxJava types from Compose:
implementation("androidx.compose.runtime:runtime-rxjava3:<version>")
}

A missing dependency shows up as a compile error such as Unresolved reference: io.reactivex. kotlinx-coroutines-core is now an API dependency of the SDK.

Keep your RxJava call sites

Use the kotlinx-coroutines-rx3 adapters to turn SDK results into the RxJava types your app expects:

SDK resultAdapter
Suspending function returning TrxSingle { sdkCall() }
Suspending function returning T?rxMaybe { sdkCall() }
Suspending function returning UnitrxCompletable { sdkCall() }
Flow<T>flow.asFlowable()/flow.asObservable()
disposables.add(
rxSingle { PdfDocumentLoader.openDocumentAsync(context, source) }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ document -> showDocument(document) },
{ error -> showError(error) },
)
)

Java code can keep its subscriptions by calling a small Kotlin adapter:

object FormProviderRxAdapters {
@JvmStatic
fun getFormFields(provider: FormProvider): Single<List<FormField>> =
rxSingle { provider.getFormFields() }
}

For SDK methods returning Flow, Java can call the adapter directly with RxConvertKt.asFlowable(flow, EmptyCoroutineContext.INSTANCE). The Catalog’s RxJava interop example runs these adapters against real SDK APIs, and its Custom Search UI (Views) example consumes a Flow from Java.

Check the result shape of each replacement API in the table below, because some former Observable APIs now return a complete list. An RxJava subscribeOn doesn’t select the dispatcher the adapter’s body runs on.

Convert to coroutines

RxJavaCoroutines
Single<T>Suspending function returning T
Maybe<T>Suspending function returning T?. An empty result is null.
CompletableSuspending function returning Unit
Observable<T>/Flowable<T>Usually Flow<T>. Some APIs return a list from a suspending function.
Disposable/CompositeDisposableA Job, or a scope with the same lifetime as the work
.subscribe(onSuccess, onError)A suspending call inside try/catch

Before:

disposables.add(
PdfDocumentLoader.openDocumentAsync(context, source)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ document -> showDocument(document) },
{ error -> showError(error) },
)
)

After:

lifecycleScope.launch {
try {
// The SDK dispatches document loading internally.
val document = PdfDocumentLoader.openDocumentAsync(context, source)
showDocument(document)
} catch (cancellation: CancellationException) {
throw cancellation
} catch (error: Exception) {
showError(error)
}
}

Replace a stream subscription with collect, and keep the Job if you need to cancel it independently, such as when starting a new search:

searchJob = lifecycleScope.launch {
textSearch.performSearchAsync(query).collect { result -> add(result) }
}

Most methods keep their names. These changed name or shape:

Before (RxJava)After
AnnotationProvider.getAnnotationsObservable(pageIndex)suspend getAnnotations(pageIndex): List<Annotation>
AnnotationProvider.getAllAnnotationsOfTypeObservable(…): Observable<Annotation>suspend getAllAnnotationsOfType(…): List<Annotation>
AnnotationProvider.addAnnotationToPageCompletable(annotation)suspend addAnnotationToPage(annotation)
AnnotationProvider.createAnnotationFromInstantJsonSingle(json)suspend createAnnotationFromInstantJson(json): Annotation
PdfDocument.documentSignatureInfoAsync/outlineAsync (properties)suspend getDocumentSignatureInfoAsync()/getOutlineAsync()
FormProvider synchronous members and …Async methods, such as getFormFields() and getFormFieldsAsync()One suspend function each, such as getFormFields(): List<FormField>
SignatureFormField.removeSignatureAsync()suspend removeSignature()
SignatureFormElement.getOverlappingSignatureAsync()suspend getOverlappingSignature(): Annotation?
DigitalSignatureValidator.validateSignatureAsync(info)suspend validateSignature(info)
DigitalSignatureInfo.validate() (blocking)suspend validate()
AbstractFormAction.getFormFieldsAsync(), HideAction.getAnnotationsAsync(), BookmarkProvider.getBookmarksAsync(), getShareIntentsAsync(), getShareTargetsAsync() (Observable)suspend functions returning a List
BitmapUtils.decodeBitmapAsync(context, uri)Top-level suspend decodeBitmapAsync(context, uri): Bitmap
AudioRecordingController.getVisualizerFlowable()getVisualizerFlow(): Flow<ByteBuffer>
TextSearch.performSearchAsync(), PdfProcessor.processDocumentAsync() (Flowable)Return a Flow
DownloadJob.getProgress(): Flowable<Progress>progress: Flow<Progress> (getProgress() in Java)
DocumentSharingController.onSharingStarted(Disposable)onSharingStarted(Job)

The synchronous TextSearch.performSearch() and PdfProcessor.processDocument() overloads are unchanged.

Interfaces and hooks you implement

Update the overrides as well as the call sites of these:

BeforeAfter
ProgressDataProvider.observeProgress(): Flowable<Double>observeProgress(): Flow<Double>
PdfOutlineView.DocumentOutlineProvider.getOutlineElements(): Single<List<OutlineElement>>getOutlineElements(): List<OutlineElement>, called on a worker thread
PdfFragment.openDocumentAsync() (protected)protected suspend fun openDocument(): PdfDocument
PdfFragment.getDocumentLoadingProgressObservables() (protected)getDocumentLoadingProgressFlows(): List<Flow<Double>>
FilePicker.getDestinationUri(…)suspend getDestinationUri(…): Uri?
PdfThumbnailBar.ConvertToDrawableRemoved

A Flow backing a progress stream must still complete after emitting its final value. Neither MutableSharedFlow nor MutableStateFlow ever completes, so don’t return one directly. For example, keep the latest progress and terminal state in a MutableStateFlow, and return state.transformWhile { … }, which stops after the terminal state. Java implementations of FilePicker should move the coroutine boundary into Kotlin.

Instant

Before (RxJava)After
InstantClient.openDocumentAsync(jwt): Single<InstantPdfDocument>suspend openDocumentAsync(jwt): InstantPdfDocument
InstantDocumentDescriptor.openDocumentAsync(jwt): Single<InstantPdfDocument>suspend openDocumentAsync(jwt): InstantPdfDocument
InstantDocumentDescriptor.downloadDocumentAsync(jwt): Flowable<InstantProgress>downloadDocumentAsync(jwt): Flow<InstantProgress>
InstantPdfDocument.reauthenticateWithJwtAsync(jwt): Completablesuspend reauthenticateWithJwtAsync(jwt)
InstantPdfDocument.syncAnnotationsAsync(): Flowable<InstantProgress>syncAnnotationsAsync(): Flow<InstantProgress>

Removed bridges

AnnotationProvider moved to suspending functions in 11.0, which kept two deprecated bridges to its previous API. Both are removed:

  • AnnotationProviderRxJava, with the …Observable, …Completable, and …Single extensions listed in the table above. Replace them with adapters in your app, as shown above.
  • AnnotationProviderBlocking, with getAnnotationsBlocking(), getAnnotationBlocking(), getAllAnnotationsOfTypeBlocking(), addAnnotationToPageBlocking(), and removeAnnotationFromPageBlocking(). Call the suspending AnnotationProvider function of the same name without Blocking from a coroutine.

For a deliberate blocking call on a worker thread from Java, use a synchronous overload where one exists, or BuildersKt.runBlocking. Never block the main thread this way.

Behavior changes to check

A successful compile doesn’t verify threading, cancellation, or empty results. Review these when you convert code:

  • Blocking work in your app stays on its own dispatcher. Declaring a function suspend doesn’t move its body off the calling thread. Wrap application-owned blocking I/O in withContext(Dispatchers.IO), and don’t wrap every SDK call by default.
  • Empty results run the following code. An empty Maybe skipped the success consumer, but a nullable suspending result returns null and execution continues. Guard it — for example, formProvider.getFormElementWithName("Name")?.let { … }.
  • Rethrow cancellation. catch (Exception) also catches CancellationException, which would show an error during normal teardown. Rethrow it before handling other failures.
  • Global error handling doesn’t carry over. RxJavaPlugins.setErrorHandler doesn’t receive failures from converted calls. Use a CoroutineExceptionHandler at your coroutine entry points.
  • Cleanup timing differs. A coroutine’s finally also runs on cancellation, unlike doAfterTerminate. Use withContext(NonCancellable) for cleanup that must finish.
  • Shared streams need a deliberate replacement. shareIn() doesn’t deliver completion or errors to its collectors the way share() did, so decide how subscribers should see them before replacing a shared stream.

Color schemes set in code

The sections below cover each change to color schemes set in code, from the new setColorScheme() method to the changed default colors. Work through the ones that apply to your integration.

setColorScheme() replaces setMainToolbarColorScheme()

PdfActivity.setMainToolbarColorScheme() is deprecated in favor of setColorScheme(), which PdfActivity and PdfUiFragment both have through PdfUi. The deprecated method still colors only the main toolbar, and its scheme wins over setColorScheme() there. When you switch, stop calling it, or call it with null, or the main toolbar keeps its old colors:

// Before:
setMainToolbarColorScheme(scheme)
// After:
setColorScheme(scheme)

setColorScheme() colors each screen from its part of the UiColorScheme, not only the toolbar:

ScreenUiColorScheme property
Main toolbarmainToolbar
Outline view and its listsoutline (new)
Share menuactionMenu (new)
Document infodocumentInfoColorScheme
Document tab bartabBar
Settings and the custom stamp creator’s switchessettingsColorScheme
AI AssistantaiAssistantColorScheme
Annotation creation toolbarcontextualToolbar (new)
Thumbnail barthumbnailBar (new)
Thumbnail gridthumbnailGrid (new)

Check which defaults your scheme starts from before you switch. A scheme copied from UiColorScheme.fromXmlTheme(context) keeps the other screens looking as they do today. A scheme copied from UiColorScheme.default() moves them from their XML theme attributes to Material 3 colors. Set the parts you want, or copy them from UiColorScheme.fromXmlTheme(context). Outside composition, such as in onCreate() or from Java, use fromXmlTheme(context); the overload without arguments is @Composable.

A scheme set with setColorScheme(UiColorScheme) applies in both light and dark mode. To follow the activity’s theme, pass a pair, either two UiColorSchemes or two Material 3 ColorSchemes, such as setColorScheme(dynamicLightColorScheme(this), dynamicDarkColorScheme(this)). The SDK picks the scheme by whether the activity’s theme is light, so a DayNight theme switches with the device. The dark theme applied by ThemeMode.NIGHT shows the dark scheme. The SDK’s default theme is light only: To show the dark scheme on a dark device, pass a DayNight theme to PdfActivityConfiguration.Builder.theme() or use ThemeMode.NIGHT. If you set the scheme again from a configuration change listener, you can remove the listener.

NutrientTheme and the deprecated theme APIs

NutrientThemeController and rememberNutrientThemeController() are deprecated, and NutrientTheme no longer takes a controller. The overloads that take one are hidden, so Kotlin code that passes controller = no longer compiles. Remove the argument:

// Before:
NutrientTheme(controller = rememberNutrientThemeController(), colorScheme = scheme) { … }
// After:
NutrientTheme(colorScheme = scheme) { … }

Calls to NutrientThemeController.invalidate() after Activity.setTheme() or a configuration change can be removed as well: PdfActivity and PdfUiFragment reapply the theme to every NutrientTheme in the activity themselves, and a scheme set with setColorScheme() is Compose state. Code compiled against 12.0 keeps linking. Code that remembers a value read from the XML theme should key it on LocalNutrientThemeKey, which changes when the activity’s theme changes in place. LocalContext and LocalConfiguration don’t change on setTheme().

UiTheme, SdkTheme, getUiColors(), getComposeUiColors(), and NutrientFloatingToolbarSearchTheme are deprecated. They keep working, and NutrientTheme, which gains an iconScheme parameter, replaces them:

DeprecatedReplacement
UiTheme.colors/UiTheme.iconsNutrientTheme.colorScheme/NutrientTheme.iconScheme
SdkTheme(colors, icons), SdkTheme.default()NutrientTheme(colorScheme = colors, iconScheme = icons) { … }
getUiColors()UiColorScheme.fromXmlTheme()
getComposeUiColors()UiColorScheme.default()
NutrientFloatingToolbarSearchTheme { … }NutrientTheme(colorScheme = floatingToolbarSearchColorScheme()) { … }
// Before:
NutrientTheme(colorScheme = getUiColors().copy(mainToolbar = myToolbarColors)) {
val background = UiTheme.colors.mainToolbar.backgroundColor
}
// After:
NutrientTheme(colorScheme = UiColorScheme.fromXmlTheme().copy(mainToolbar = myToolbarColors)) {
val background = NutrientTheme.colorScheme.mainToolbar.backgroundColor
}

NutrientTheme and every SDK screen now pick their colors in this order:

  1. The colorScheme passed to NutrientTheme, or to a NutrientTheme enclosing it. It applies only to that NutrientTheme’s content, NutrientSettingsView included; SDK sheets and dialogs keep the activity’s scheme.
  2. The scheme set with PdfActivity.setColorScheme() or PdfUiFragment.setColorScheme().
  3. The XML theme attributes of the activity, or of a theme overlay around the NutrientTheme, such as android:theme on its view or a ContextThemeWrapper.

So NutrientTheme { … } without a colorScheme now matches the SDK screens of its activity. The SDK reapplies the theme on PdfActivity.setTheme(), and the XML colors also follow a night mode switch the activity handles itself, so neither requires recreating the activity. In another activity hosting a PdfUiFragment, setTheme() reaches the SDK screens opened afterward; recreate the activity to apply the new theme to those already shown.

NutrientTheme now installs a MaterialTheme around its content, using UiColorScheme.material and the enclosing theme’s typography and shapes. Compose content inside NutrientTheme therefore reads the SDK’s colors from MaterialTheme.colorScheme. If you wrap SDK composables in your own MaterialTheme and want its colors to apply, pass UiColorScheme.fromMaterial(MaterialTheme.colorScheme) as the colorScheme of NutrientTheme. Secondary text and switch icons in the settings sheet now use SettingsColorScheme.labelTextColor instead of titleTextColor; the sheet’s drag handle keeps the title color.

Color scheme classes and their Material 3 base

UiColorScheme has a Material 3 ColorScheme base. UiColorScheme.fromMaterial() and UiColorScheme.fromXmlTheme(Context) build a scheme outside of composition, and any color you leave unspecified takes its Material role when the scheme is resolved.

The color scheme classes (UiColorScheme, MainToolbarColors, SettingsColorScheme, TabBarColors, DocumentInfoColorScheme, AiAssistantColorScheme, and the new ones) are no longer data classes. copy() keeps its signature, and equals() and hashCode() still compare by value, now including the Material base. Destructuring them (component1() and so on) no longer compiles.

UiColorScheme has a new first property, material: ColorScheme. The 11.7 four-argument and 12.0 five-argument positional constructors keep compiling and linking, and they derive the Material base from the slices you pass. Longer positional forms must name their arguments.

Schemes built with those two constructors look different, because the screens they don’t set now follow that derived base. It takes its background and text from documentInfoColorScheme and its accent from settingsColorScheme.selectedColor. Its bar color comes from the main toolbar background, and every other role comes from the light or dark Material baseline:

  • The document tab bar no longer copies the main toolbar (TabBarColors.from(mainToolbar)) in the four-argument form. It takes the document info background, with secondaryContainer for the selected tab and onSurfaceVariant for the other titles.
  • The outline view, share menu, and thumbnail grid take the document info background and text, with settingsColorScheme.selectedColor as their accent. The outline’s unselected tab icons and the thumbnail grid’s background take baseline roles.
  • The annotation creation toolbar and thumbnail bar keep the main toolbar background. Their icons, borders, thumbnail borders, and submenu take baseline roles instead of the toolbar’s icon and text colors. With a dark main toolbar over a light document info sheet, set them explicitly.

If you use these constructors, set tabBar, contextualToolbar, and thumbnailBar explicitly, and outline if its accent should differ — for example, UiColorScheme(mainToolbar, settings, aiAssistant, documentInfo).copy(tabBar = myTabBar, contextualToolbar = myContextualToolbar, thumbnailBar = myThumbnailBar). Alternatively, start from UiColorScheme.fromMaterial() with your own Material scheme.

Every slice color now defaults to Color.Unspecified and resolves to its Material role in NutrientTheme. A color that defaults to another color of its slice now takes it at resolution too. For example, MainToolbarColors.iconColor defaults to textColor, so copy(textColor = …) also changes the icons. Read colors through NutrientTheme.colorScheme inside composition, or call UiColorScheme.resolve() outside it; a slice read directly off a scheme built with fromMaterial() is unspecified until then. UiColorScheme.default() still returns resolved colors, and NutrientTheme.colorScheme read outside any NutrientTheme returns the light Material baseline instead of unspecified colors.

The OutlineColors.from(), ActionMenuColors.from(), ContextualToolbarColors.from(), ThumbnailGridColors.from(), and ThumbnailBarColors.from() helpers are removed, and TabBarColors.from() is deprecated. Leave the slice unspecified instead, and it follows the Material base.

Annotation creation toolbar and thumbnail bar

In 12.0, NutrientContextualToolbar, NutrientContextualToolbarDefaults.colors(), sideDockedBorder(), and the thumbnail bar always used their XML style colors (pspdf__contextualToolbarStyle, pspdf__thumbnailBarStyle, and pspdf__scrollableThumbnailBarStyle), whatever the scheme. They now use the scheme’s contextualToolbar and thumbnailBar colors:

  • A scheme from UiColorScheme.fromXmlTheme(), or no scheme at all, looks the same as before.
  • UiColorScheme.default() gives a neutral Material bar: a surfaceContainer background, onSurfaceVariant icons, outline thumbnail borders, and primary selection and current page.
  • A scheme built with the UiColorScheme constructors puts both bars on its mainToolbar background. Icons, borders, and the submenu come from the scheme’s Material base.

To keep the XML colors under another scheme, copy them in: scheme.copy(contextualToolbar = UiColorScheme.fromXmlTheme(context).contextualToolbar, thumbnailBar = UiColorScheme.fromXmlTheme(context).thumbnailBar). PdfThumbnailBar.setBackgroundColor(), setThumbnailBorderColor(), and setSelectedThumbnailBorderColor() still take precedence over the scheme, and a new border color now rerenders thumbnails already on screen.

The deprecated setMainToolbarColorScheme() no longer reaches the contextual toolbar’s text items or slot content; set contextualToolbar on the scheme instead. NutrientContextualToolbar has a new trailing submenuBorderColor parameter, so code compiled against 12.0 that calls it must be rebuilt. A host that passes its own colors can now match the submenu border with it.

SettingsColorScheme icon colors

SettingsColorScheme has two new required properties — unselectedIconColor and disabledIconColor — and getSettingsColorScheme() has two matching parameters. Code that constructs a SettingsColorScheme must pass them, and code compiled against 12.0 that calls its constructor, copy(), or getSettingsColorScheme() must be rebuilt:

// 12.0:
SettingsColorScheme(selected, unselected, unselectedText, background, divider, title, label)
// 12.1:
SettingsColorScheme(selected, unselected, unselectedText, background, divider, title, label, unselectedIcon, disabledIcon)

In 12.0, the settings option icons always used the pspdf__settingsModePickerItemStyle tint. They now use the scheme’s colors: UiColorScheme.fromXmlTheme() keeps that tint, and UiColorScheme.default() uses onSurfaceVariant and onSurface at 38 percent opacity. To keep the tint under another scheme, copy the two colors from UiColorScheme.fromXmlTheme(context).settingsColorScheme.

Share menu icon tint

The share menu’s fixed action icons from a drawable resource were always tinted with the XML theme’s pspdf__fixedActionsIconColor, white by default. They now take their tint from ActionMenuColors.fixedActionsIconColor, which defaults to onPrimary or the XML attribute, so a scheme set in code can change it. A Drawable passed to FixedActionMenuItem(itemId, icon, label) is still drawn in its own colors. If your scheme set a dark fixedActionsIconBackgroundColor only to keep white icons readable, you can use your accent again. The icon color then follows the accent.

Changed default colors

The defaults of getMainToolbarColors(), getAiAssistantColorScheme(), getDocumentInfoColorScheme(), and getSettingsColorScheme(), and therefore UiColorScheme.default(), map to different Material roles than in 12.0, which fixes hard-to-read and reversed colors:

  • Main toolbar — The background is surfaceContainer instead of background. Text and title are onBackground instead of onPrimary. The overflow menu background is surfaceContainer instead of primaryContainer.
  • AI Assistant — Text field text and hint are onBackground and onSurfaceVariant instead of onSecondary.
  • Document info — Group titles are primary instead of onBackground. Values, hints, and group icons are onSurfaceVariant instead of primary. The outline tabs follow these colors, so the selected tab is primary and the unselected tabs are onSurfaceVariant.
  • Outline — Swipe-to-delete uses error with an onError icon instead of primaryContainer. The new OutlineColors.deleteIconColor sets the icon color.
  • Settings — Dividers are outlineVariant instead of onPrimaryContainer.

To keep the 12.0 colors, pass them explicitly — for example, getMainToolbarColors(textColor = MaterialTheme.colorScheme.onPrimary) — or copy() the parts of UiColorScheme.default() you want to keep.

SettingsColorScheme.forSwitch(), which colors the date and time switches in the custom stamp creator, also returns different colors. A checked switch now has a solid selectedColor track with a background thumb instead of a 30 percent selectedColor track with a selectedColor thumb. An unchecked switch now has an unselectedColor border instead of a selectedColor one. To keep the 12.0 look, pass your own SwitchDefaults.colors(…) — for example, with checkedTrackColor = settings.selectedColor.copy(alpha = 0.3f) and checkedThumbColor = settings.selectedColor.

Text selection handle colors

Text selection handles that pspdf__leftHandleColor and pspdf__rightHandleColor don’t color are now derived from pspdf__highlightColor: darker on a light page and lighter on an inverted page. If you override only pspdf__highlightColor in a style extending PSPDFKit.TextSelection, your handles now follow it instead of using the SDK’s default blue. To keep a fixed handle color, set both handle attributes explicitly.

Default maximum zoom scale

The default maximum zoom scale, PdfFragment.MAX_ZOOM, is now 50 instead of 40. To keep the previous limit, call maxZoomScale(40f) on PdfConfiguration.Builder or PdfActivityConfiguration.Builder.

Electronic signatures

Signature.createInkSignature(), Signature.createStampSignature(), and the ElectronicSignatureOptions constructor gained new optional parameters for the signature’s opacity and, for the options, thickness. Source code compiles unchanged, and Java callers keep their existing overloads. Kotlin code compiled against 12.0 that calls them with default arguments, such as a prebuilt library, must be rebuilt against 12.1. The same applies to ElectronicSignatureOptions.copy(), because the data class gained the two new properties.

PdfActivityConfiguration.Builder.forceSignatureButtonPositionInMainToolbar() and PdfActivityConfiguration.isSignatureButtonPositionForcedInMainToolbar are deprecated and will be removed in a future version. To show the signature button in the main toolbar, append it to the default toolbar items instead. Check the default items first, because the SDK already includes the button there for some licenses:

// Before:
PdfActivityConfiguration.Builder(context)
.forceSignatureButtonPositionInMainToolbar(true)
.build()
// After, in your `PdfActivity` or on your `PdfUiFragment`:
setToolbarItemsProvider { defaultItems ->
if (defaultItems.any { it.id == PdfActivity.MENU_OPTION_SIGNATURE }) {
defaultItems
} else {
defaultItems + ToolbarItem.Button(
actionId = resources.getResourceEntryName(PdfActivity.MENU_OPTION_SIGNATURE),
id = PdfActivity.MENU_OPTION_SIGNATURE,
contentDescription = getString(com.pspdfkit.R.string.pspdf__signature),
icon = ToolbarIcon.Resource(com.pspdfkit.R.drawable.pspdf__ic_signature),
)
}
}

DownloadProgressFragment

The protected dialog field of DownloadProgressFragment is now the downloadDialog property. Subclasses that read or replace the displayed dialog need to use the new name, or getDownloadDialog() and setDownloadDialog() from Java. Java code that uses dialog no longer compiles. Kotlin code that only reads dialog still compiles, because the name now resolves to DialogFragment.getDialog(). That’s the same dialog while it’s shown, but switch to downloadDialog so your code doesn’t depend on that:

// Before:
dialog?.dismiss()
// After:
downloadDialog?.dismiss()