This guide demonstrates different usage patterns and scenarios for platform adapters through practical examples.

Example overview

ExampleDescriptionKey concepts
Basic usageSimplest PDF viewer implementationNutrientView, initialization
Configuration examplesPlatform-specific configurationAdapters, configureFragment, configureView, configureLoad
Controller implementationProgrammatic document controlCustom controllers, typed interface, lifecycle
Event listenersResponding to document eventsPlatform callbacks, lifecycle events
UI customizationCustomizing toolbars and menusToolbar configuration, menu items, theme customization

1. Basic usage

This is the simplest way to display a PDF document without any customization:

import 'package:flutter/material.dart';
import 'package:nutrient_flutter/nutrient_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Nutrient SDK.
await Nutrient.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('PDF Viewer')),
body: const NutrientView(
documentPath: 'assets/sample.pdf',
),
),
);
}
}

Use case: Quick prototyping, simple PDF display without customization.

2. Configuration examples

Configure the PDF viewer using platform-specific adapters:

import 'package:nutrient_flutter_android/src/android_platform_adapter.dart';
import 'package:nutrient_flutter_android/src/bindings/nutrient_android_sdk_bindings.dart'
as android;
class ConfigAndroidAdapter extends AndroidAdapter {
@override
Future<void> configureFragment(
NutrientViewHandle handle,
android.PdfUiFragmentBuilder builder,
android.Context context,
) async {
await super.configureFragment(handle, builder, context);
// Create comprehensive configuration.
final configuration = android.PdfActivityConfiguration$Builder(context)
// Layout & Display
.layoutMode(android.PageLayoutMode.SINGLE)
.scrollDirection(android.PageScrollDirection.VERTICAL)
.fitMode(android.PageFitMode.FIT_TO_WIDTH)
.firstPageAlwaysSingle(true)
.showGapBetweenPages(true)
.restoreLastViewedPage(true)
// UI Features
.setThumbnailBarMode(
android.ThumbnailBarMode.THUMBNAIL_BAR_MODE_SCROLLABLE)
.searchEnabled(true)
.outlineEnabled(true)
.pageNumberOverlayEnabled(true)
// Annotations
.annotationEditingEnabled(true)
.annotationListEnabled(true)
// Document Operations
.textSelectionEnabled(true)
.printingEnabled(true)
.undoEnabled(true)
.redoEnabled(true)
.autosaveEnabled(true)
.build();
builder.configuration(configuration);
}
@override
Future<void> onFragmentAttached(
android.PdfUiFragment fragment,
android.Context context,
) async {}
@override
Future<void> onPdfFragmentReady(android.PdfFragment pdfFragment) async {}
@override
Future<void> onFragmentDetached() async {}
}

For more information, refer to information about Android viewer configuration.

Using adapters with NutrientView

import 'package:flutter/material.dart';
import 'package:nutrient_flutter/nutrient_flutter.dart';
// Conditional import resolves to the correct platform factory.
import 'adapters/adapters.dart';
class ConfiguredPdfViewer extends StatelessWidget {
final String documentPath;
const ConfiguredPdfViewer({super.key, required this.documentPath});
@override
Widget build(BuildContext context) {
final adapter = createAdapter();
return NutrientView(
documentPath: documentPath,
adapter: adapter as NutrientPlatformAdapter?,
);
}
}

Use case: Setting up viewer behavior — page layout, scroll direction, enabled features, annotation settings.

3. Controller implementation

Without a controller, the PDF viewer accepts configuration at initialization but has no programmatic interface afterward. A custom controller provides methods for navigating to specific pages, retrieving document information, reacting to document events, and performing any operation the native SDK supports.

Define a controller interface

Keep the interface pure (methods only) and define callbacks separately. Callbacks are injected via the adapter constructor:

lib/adapters/my_controller.dart
/// Callback for document info updates.
typedef DocumentInfoCallback = void Function({
int? pageCount,
int? currentPage,
String? title,
bool? isReady,
});
/// Callback for status/event messages from the adapter.
typedef StatusChangedCallback = void Function(String status);
/// Controller interface for cross-platform document APIs.
abstract class MyController {
StatusChangedCallback? get onStatusChanged;
DocumentInfoCallback? get onDocumentInfo;
Future<int> getPageCount();
Future<int> getCurrentPageIndex();
Future<void> setPageIndex(int pageIndex);
Future<String?> getDocumentTitle();
}

Implement in platform adapters

Each adapter extends its base adapter class and implements the controller interface. Callbacks use ?.call() since they’re optional. Call markReady() when the document is loaded.

import 'package:nutrient_flutter_android/src/android_platform_adapter.dart';
import 'package:nutrient_flutter_android/src/bindings/nutrient_android_sdk_bindings.dart'
as android;
import 'my_controller.dart';
class MyAndroidAdapter extends AndroidAdapter
implements MyController {
@override
final StatusChangedCallback? onStatusChanged;
@override
final DocumentInfoCallback? onDocumentInfo;
android.PdfDocument? _document;
android.PdfFragment? _pdfFragment;
android.DocumentListener? _documentListener;
int _currentPageIndex = 0;
MyAndroidAdapter({
this.onStatusChanged,
this.onDocumentInfo,
});
@override
Future<void> onFragmentAttached(
android.PdfUiFragment fragment,
android.Context context,
) async {}
@override
Future<void> onPdfFragmentReady(
android.PdfFragment pdfFragment) async {
_pdfFragment = pdfFragment;
// Check if document is already loaded.
final document = pdfFragment.getDocument();
if (document != null) {
_handleDocumentLoaded(document);
}
// Set up document listener for lifecycle events.
_documentListener = android.DocumentListener.implement(
android.$DocumentListener(
onDocumentLoaded: (document) =>
_handleDocumentLoaded(document),
onDocumentLoadFailed: (exception) {
onStatusChanged?.call('Load failed');
},
onPageChanged: (document, pageIndex) {
_currentPageIndex = pageIndex;
onDocumentInfo?.call(currentPage: pageIndex);
},
// Required callbacks.
onDocumentSave: (document, options) => true,
onDocumentSaved: (document) {},
onDocumentSaveFailed: (document, exception) {},
onDocumentSaveCancelled: (document) {},
onPageClick:
(document, pageIndex, event, point, annotation) =>
false,
onDocumentClick: () => false,
onDocumentZoomed: (document, pageIndex, zoom) {},
onPageUpdated: (document, pageIndex) {},
),
);
pdfFragment.addDocumentListener(_documentListener!);
}
void _handleDocumentLoaded(android.PdfDocument document) {
_document = document;
_currentPageIndex = _pdfFragment?.getPageIndex() ?? 0;
onDocumentInfo?.call(
pageCount: document.getPageCount(),
currentPage: _currentPageIndex,
title: document
.getTitle()
?.toDartString(releaseOriginal: true) ??
'Untitled',
isReady: true,
);
markReady();
onStatusChanged?.call('Document loaded via JNI');
}
@override
Future<int> getPageCount() async =>
_document?.getPageCount() ?? 0;
@override
Future<int> getCurrentPageIndex() async =>
_currentPageIndex;
@override
Future<void> setPageIndex(int pageIndex) async {
_currentPageIndex = pageIndex;
_pdfFragment?.setPageIndex$1(pageIndex, true);
}
@override
Future<String?> getDocumentTitle() async => _document
?.getTitle()
?.toDartString(releaseOriginal: true);
@override
Future<void> onFragmentDetached() async {
if (_documentListener != null && _pdfFragment != null) {
_pdfFragment!
.removeDocumentListener(_documentListener!);
}
_documentListener = null;
_pdfFragment = null;
_document = null;
}
}

Use with NutrientView

import 'package:flutter/material.dart';
import 'package:nutrient_flutter/nutrient_flutter.dart';
// Conditional import resolves to the correct platform factory.
import 'adapters/adapters.dart';
class ControllerExample extends StatefulWidget {
final String documentPath;
const ControllerExample({super.key, required this.documentPath});
@override
State<ControllerExample> createState() => _ControllerExampleState();
}
class _ControllerExampleState extends State<ControllerExample> {
MyController? _controller;
String _status = 'Initializing...';
int? _pageCount;
int? _currentPage;
bool _isReady = false;
@override
void initState() {
super.initState();
_controller = createAdapter(
onStatusChanged: _updateStatus,
onDocumentInfo: _updateDocumentInfo,
);
}
void _updateStatus(String status) {
if (mounted) setState(() => _status = status);
}
void _updateDocumentInfo({
int? pageCount,
int? currentPage,
String? title,
bool? isReady,
}) {
if (mounted) {
setState(() {
if (pageCount != null) _pageCount = pageCount;
if (currentPage != null) _currentPage = currentPage;
if (isReady != null) _isReady = isReady;
});
}
}
void _goToNextPage() async {
if (_controller != null && _currentPage != null && _pageCount != null) {
final nextPage = (_currentPage! + 1) % _pageCount!;
await _controller!.setPageIndex(nextPage);
}
}
void _goToPreviousPage() async {
if (_controller != null && _currentPage != null && _pageCount != null) {
final prevPage = (_currentPage! - 1 + _pageCount!) % _pageCount!;
await _controller!.setPageIndex(prevPage);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_pageCount != null && _currentPage != null
? 'Page ${_currentPage! + 1} of $_pageCount'
: _status),
actions: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _isReady ? _goToPreviousPage : null,
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _isReady ? _goToNextPage : null,
),
],
),
body: NutrientView(
documentPath: widget.documentPath,
adapter: _controller as NutrientPlatformAdapter?,
),
);
}
}

Controller lifecycle

  1. StatefulWidget.initState() creates the adapter via the platform factory function.
  2. The adapter is stored as the controller interface type.
  3. NutrientView receives the adapter cast to NutrientPlatformAdapter.
  4. The platform view loads and adapter lifecycle methods fire.
  5. The document loads, and the adapter calls onDocumentInfo(isReady: true).
  6. The adapter calls markReady() to signal that controller methods are safe to call.
  7. Controller methods (getPageCount(), setPageIndex(), etc.) are now available.

Use case: Building custom UI controls, programmatic navigation, accessing document properties, building document management features.

4. Event listeners

Listen to native document and UI events.

import 'package:flutter/foundation.dart';
import 'package:nutrient_flutter_android/src/android_platform_adapter.dart';
import 'package:nutrient_flutter_android/src/bindings/nutrient_android_sdk_bindings.dart'
as android;
class EventAndroidAdapter extends AndroidAdapter {
android.DocumentListener? _documentListener;
android.PdfFragment? _pdfFragment;
void _log(String message) =>
debugPrint('[Events-Android] $message');
@override
Future<void> onFragmentAttached(
android.PdfUiFragment fragment,
android.Context context,
) async {}
@override
Future<void> onPdfFragmentReady(
android.PdfFragment pdfFragment) async {
_pdfFragment = pdfFragment;
_documentListener =
android.DocumentListener.implement(
android.$DocumentListener(
onDocumentLoaded: (document) {
_log('Document loaded: '
'${document.getPageCount()} pages');
},
onDocumentLoadFailed: (exception) {
_log('Load failed');
},
onPageChanged: (document, pageIndex) {
_log('Page changed to $pageIndex');
},
onDocumentZoomed: (document, pageIndex, zoom) {
_log('Zoomed to '
'${zoom.toStringAsFixed(2)}x');
},
onDocumentSave: (document, options) => true,
onDocumentSaved: (document) {
_log('Document saved');
},
onDocumentSaveFailed: (document, exception) {
_log('Save failed');
},
onDocumentSaveCancelled: (document) {
_log('Save cancelled');
},
onPageClick: (document, pageIndex, event,
point, annotation) =>
false,
onDocumentClick: () => false,
onPageUpdated: (document, pageIndex) {
_log('Page $pageIndex updated');
},
),
);
pdfFragment.addDocumentListener(_documentListener!);
}
@override
Future<void> onFragmentDetached() async {
if (_documentListener != null &&
_pdfFragment != null) {
_pdfFragment!
.removeDocumentListener(_documentListener!);
}
_documentListener = null;
_pdfFragment = null;
}
}

For more information, refer to the Android events guide.

Use case: Logging, analytics, responding to document state changes, cleanup operations.

5. UI customization

Customize toolbars and menus using platform-specific adapter hooks.

import 'package:jni/jni.dart' as jni;
import 'package:nutrient_flutter_android/src/android_platform_adapter.dart';
import 'package:nutrient_flutter_android/src/bindings/nutrient_android_sdk_bindings.dart'
as android;
class UIAndroidAdapter extends AndroidAdapter {
android.PdfUiFragment? _pdfUiFragment;
android
.ToolbarCoordinatorLayout$OnContextualToolbarLifecycleListener?
_toolbarListener;
@override
Future<void> configureFragment(
NutrientViewHandle handle,
android.PdfUiFragmentBuilder builder,
android.Context context,
) async {
await super.configureFragment(
handle, builder, context);
final config =
android.PdfActivityConfiguration$Builder(context)
.setEnabledShareFeatures(
android.ShareFeatures.all())
.documentTitleOverlayEnabled(true)
.pageNumberOverlayEnabled(true)
.setThumbnailBarMode(android.ThumbnailBarMode
.THUMBNAIL_BAR_MODE_SCROLLABLE)
.annotationEditingEnabled(true)
.annotationListEnabled(true)
.searchEnabled(true)
.inlineSearchEnabled(true)
.build();
builder.configuration(config);
}
@override
Future<void> onFragmentAttached(
android.PdfUiFragment fragment,
android.Context context,
) async {
_pdfUiFragment = fragment;
}
@override
Future<void> onPdfFragmentReady(
android.PdfFragment pdfFragment) async {
// Listen for contextual toolbar lifecycle events.
if (_pdfUiFragment == null) return;
_toolbarListener =
android
.ToolbarCoordinatorLayout$OnContextualToolbarLifecycleListener
.implement(
android
.$ToolbarCoordinatorLayout$OnContextualToolbarLifecycleListener(
onPrepareContextualToolbar:
(android.ContextualToolbar<jni.JObject?>
toolbar) {
final itemCount =
toolbar.getMenuItems().length;
// Modify menu items here if needed.
},
onDisplayContextualToolbar:
(android.ContextualToolbar<jni.JObject?>
toolbar) {},
onRemoveContextualToolbar:
(android.ContextualToolbar<jni.JObject?>
toolbar) {},
),
);
_pdfUiFragment!
.setOnContextualToolbarLifecycleListener(
_toolbarListener,
);
}
@override
Future<void> onFragmentDetached() async {
if (_pdfUiFragment != null) {
_pdfUiFragment!
.setOnContextualToolbarLifecycleListener(
null);
}
_toolbarListener = null;
_pdfUiFragment = null;
}
}

For more information, refer to the Android toolbar and menus guide.

Use case: Branding, custom toolbar layouts, hiding/showing features, adding custom buttons.

Best practices

Follow these guidelines to ensure your adapters are reliable and maintainable.

1. Call super methods

Call super methods in adapter overrides to preserve base functionality:

@override
Future<void> configureFragment(...) async {
await super.configureFragment(handle, builder, context);
// Your customization here.
}

2. Handle null safety

Native SDK objects are nullable — check before use:

final document = pdfFragment.getDocument();
if (document != null) {
final pageCount = document.getPageCount();
}

3. Implement controllers consistently

Implement the same interface on all platforms for consistency:

// All platform adapters implement the same interface.
class MyAndroidAdapter extends AndroidAdapter implements MyController { }
class MyIOSAdapter extends IOSAdapter implements MyController { }
class MyWebAdapter extends NutrientWebAdapter implements MyController { }

4. Clean up resources

Clean up listeners and references in detach callbacks:

@override
Future<void> onFragmentDetached() async {
if (_documentListener != null && _pdfFragment != null) {
_pdfFragment!.removeDocumentListener(_documentListener!);
}
_documentListener = null;
_pdfFragment = null;
_document = null;
}

Next steps