---
title: "Nutrient Android SDK 12.1 release notes"
canonical_url: "https://www.nutrient.io/guides/android/releases/12-1/"
md_url: "https://www.nutrient.io/guides/android/releases/12-1.md"
last_updated: "2026-11-10T00:00:00.000Z"
description: "Nutrient Android SDK 12.1 release notes: Kotlin coroutines instead of RxJava, theming every SDK screen from code, and signature thickness and opacity."
---

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](#migration-guide) covers every change you need to react to, with before and after code. For the full list of changes, see our [changelog](https://www.nutrient.io/guides/android/changelog.md#12.1.0).

## 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:

```kotlin

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 `UiColorScheme`s or two Material 3 `ColorScheme`s. 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:

```kotlin

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:

```kotlin

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](https://www.nutrient.io/guides/android/agent-skill.md) first. It includes the same migration steps:

```text

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:

```kotlin

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 result                           | Adapter                                   |
| ------------------------------------ | ----------------------------------------- |
| Suspending function returning `T`    | `rxSingle { sdkCall() }`                  |
| Suspending function returning `T?`   | `rxMaybe { sdkCall() }`                   |
| Suspending function returning `Unit` | `rxCompletable { sdkCall() }`             |
| `Flow<T>`                            | `flow.asFlowable()`/`flow.asObservable()` |

```kotlin

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:

```kotlin

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

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

Before:

```kotlin

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

```

After:

```kotlin

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:

```kotlin

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:

| Before                                                                                      | After                                                                   |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `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.ConvertToDrawable`                                                         | Removed                                                                 |

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): Completable`                 | `suspend 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:

```kotlin

// Before:
setMainToolbarColorScheme(scheme)

// After:
setColorScheme(scheme)

```

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

| Screen                                           | `UiColorScheme` property  |
| ------------------------------------------------ | ------------------------- |
| Main toolbar                                     | `mainToolbar`             |
| Outline view and its lists                       | `outline` (new)           |
| Share menu                                       | `actionMenu` (new)        |
| Document info                                    | `documentInfoColorScheme` |
| Document tab bar                                 | `tabBar`                  |
| Settings and the custom stamp creator’s switches | `settingsColorScheme`     |
| AI Assistant                                     | `aiAssistantColorScheme`  |
| Annotation creation toolbar                      | `contextualToolbar` (new) |
| Thumbnail bar                                    | `thumbnailBar` (new)      |
| Thumbnail grid                                   | `thumbnailGrid` (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 `UiColorScheme`s or two Material 3 `ColorScheme`s, 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:

```kotlin

// 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:

| Deprecated                                      | Replacement                                                             |
| ----------------------------------------------- | ----------------------------------------------------------------------- |
| `UiTheme.colors`/`UiTheme.icons`                | `NutrientTheme.colorScheme`/`NutrientTheme.iconScheme`                  |
| `SdkTheme(colors, icons)`, `SdkTheme.default()` | `NutrientTheme(colorScheme = colors, iconScheme = icons) { … }`         |
| `getUiColors()`                                 | `UiColorScheme.fromXmlTheme()`                                          |
| `getComposeUiColors()`                          | `UiColorScheme.default()`                                               |
| `NutrientFloatingToolbarSearchTheme { … }`      | `NutrientTheme(colorScheme = floatingToolbarSearchColorScheme()) { … }` |

```kotlin

// 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:

```kotlin

// 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:

```kotlin

// 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:

```kotlin

// Before:
dialog?.dismiss()

// After:
downloadDialog?.dismiss()

```
---

## Related pages

- [10 10 1](/guides/android/releases/10-10-1.md)
- [10 10](/guides/android/releases/10-10.md)
- [10 6](/guides/android/releases/10-6.md)
- [10 7](/guides/android/releases/10-7.md)
- [10 8](/guides/android/releases/10-8.md)
- [10 9](/guides/android/releases/10-9.md)
- [11 0](/guides/android/releases/11-0.md)
- [11 1](/guides/android/releases/11-1.md)
- [11 2](/guides/android/releases/11-2.md)
- [11 3 1](/guides/android/releases/11-3-1.md)
- [11 3](/guides/android/releases/11-3.md)
- [11 4](/guides/android/releases/11-4.md)
- [11 5](/guides/android/releases/11-5.md)
- [11 6](/guides/android/releases/11-6.md)
- [11 7](/guides/android/releases/11-7.md)
- [12 0](/guides/android/releases/12-0.md)

