Usage patterns
This guide demonstrates different usage patterns and scenarios for platform adapters through practical examples.
Example overview
| Example | Description | Key concepts |
|---|---|---|
| Basic usage | Simplest PDF viewer implementation | NutrientView, initialization |
| Configuration examples | Platform-specific configuration | Adapters, configureFragment, configureView, configureLoad |
| Controller implementation | Programmatic document control | Custom controllers, typed interface, lifecycle |
| Event listeners | Responding to document events | Platform callbacks, lifecycle events |
| UI customization | Customizing toolbars and menus | Toolbar 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.
import 'package:nutrient_flutter_ios/src/ios_platform_adapter.dart';import 'package:nutrient_flutter_ios/src/bindings/nutrient_ios_bindings.dart' as ios;
class ConfigIOSAdapter extends IOSAdapter { @override void configureView( NutrientViewHandle handle, ios.PSPDFConfigurationBuilder builder, ) { super.configureView(handle, builder);
// Layout & Display builder.pageMode = ios.PSPDFPageMode.PSPDFPageModeSingle; builder.scrollDirection = ios.PSPDFScrollDirection.PSPDFScrollDirectionVertical; builder.pageTransition = ios.PSPDFPageTransition.PSPDFPageTransitionScrollContinuous;
// UI Features builder.thumbnailBarMode = ios.PSPDFThumbnailBarMode.PSPDFThumbnailBarModeScrollable; builder.isSearchEnabled = true; builder.outlineViewEnabled = true;
// Annotations builder.editableAnnotationTypes = null; // Enable all annotation types }
@override Future<void> onViewControllerReady( ios.PSPDFViewController viewController) async { // Access document after view controller is ready. final document = viewController.document; if (document != null) { // Configure document-level settings. document.annotationSaveMode = ios.PSPDFAnnotationSaveMode.PSPDFAnnotationSaveModeEmbedded; } }
@override Future<void> onViewControllerDetached() async {}}For more information, refer to information about iOS view controller configuration.
import 'package:nutrient_flutter_web/nutrient_flutter_web.dart';
class ConfigWebAdapter extends NutrientWebAdapter { @override Future<void> configureLoad( NutrientViewHandle handle, Map<String, dynamic> config, ) async { await super.configureLoad(handle, config);
// Layout & Display config['layoutMode'] = 'SINGLE'; config['scrollMode'] = 'CONTINUOUS'; config['spreadFitting'] = 'FIT_TO_VIEWPORT'; config['initialPageIndex'] = 0;
// Theme config['theme'] = 'AUTO'; config['toolbarPlacement'] = 'TOP';
// Toolbar items config['toolbarItems'] = [ {'type': 'sidebar-thumbnails'}, {'type': 'sidebar-document-outline'}, {'type': 'sidebar-annotations'}, {'type': 'sidebar-bookmarks'}, {'type': 'spacer'}, {'type': 'pager'}, {'type': 'zoom-out'}, {'type': 'zoom-in'}, {'type': 'zoom-mode'}, {'type': 'spacer'}, {'type': 'highlighter'}, {'type': 'text-highlighter'}, {'type': 'ink'}, {'type': 'ink-eraser'}, {'type': 'signature'}, {'type': 'note'}, {'type': 'text'}, {'type': 'spacer'}, {'type': 'print'}, {'type': 'search'}, {'type': 'export-pdf'}, ];
// Features config['enableAnnotationToolbar'] = true; config['enableHistory'] = true; config['enableClipboardActions'] = true; config['enableTextSelection'] = true; config['enablePrinting'] = true; config['autoSaveMode'] = 'INTELLIGENT'; }
@override Future<void> onInstanceLoaded(NutrientWebInstance instance) async { await super.onInstanceLoaded(instance); }
@override Future<void> dispose() async { await super.dispose(); }}For more information, refer to the Web viewer guide.
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:
/// 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; }}import 'package:nutrient_flutter_ios/src/ios_platform_adapter.dart';import 'package:nutrient_flutter_ios/src/bindings/nutrient_ios_bindings.dart' as ios;
import 'my_controller.dart';
class MyIOSAdapter extends IOSAdapter implements MyController { @override final StatusChangedCallback? onStatusChanged;
@override final DocumentInfoCallback? onDocumentInfo;
ios.PSPDFViewController? _viewController; ios.PSPDFDocument? _document; ios.PSPDFViewControllerDelegate? _delegate; int _currentPageIndex = 0;
MyIOSAdapter({ this.onStatusChanged, this.onDocumentInfo, });
@override Future<void> onViewControllerReady( ios.PSPDFViewController viewController) async { _viewController = viewController;
// Set up delegate for document change and page // events. _delegate = ios.PSPDFViewControllerDelegate$Builder .implementAsListener( pdfViewController_didChangeDocument: (ios.PSPDFViewController controller, ios.PSPDFDocument? document) { _handleDocumentChanged(document); }, pdfViewController_didConfigurePageView_forPageAtIndex: (controller, pageView, pageIndex) { _currentPageIndex = pageIndex; onDocumentInfo?.call(currentPage: pageIndex); }, ); viewController.delegate = _delegate;
// Check for already-loaded document (delegate only // fires on changes). final existingDocument = viewController.document; if (existingDocument != null) { _handleDocumentChanged(existingDocument); } }
void _handleDocumentChanged(ios.PSPDFDocument? document) { _document = document; if (document == null) return;
final pageCount = document.pageCount; final title = document.title?.toDartString() ?? 'Untitled'; _currentPageIndex = _viewController?.pageIndex ?? 0;
onDocumentInfo?.call( pageCount: pageCount, currentPage: _currentPageIndex, title: title, isReady: true, ); markReady(); onStatusChanged?.call('Document loaded via FFI'); }
@override Future<int> getPageCount() async => _document?.pageCount ?? 0;
@override Future<int> getCurrentPageIndex() async => _currentPageIndex;
@override Future<void> setPageIndex(int pageIndex) async { _currentPageIndex = pageIndex; _viewController?.setPageIndex_animated(pageIndex, animated: true); }
@override Future<String?> getDocumentTitle() async => _document?.title?.toDartString();
@override Future<void> onViewControllerDetached() async { _delegate = null; _viewController = null; _document = null; }}import 'dart:js_interop';import 'package:nutrient_flutter_web/nutrient_flutter_web.dart';
import 'my_controller.dart';
class MyWebAdapter extends NutrientWebAdapter implements MyController { @override final StatusChangedCallback? onStatusChanged;
@override final DocumentInfoCallback? onDocumentInfo;
int _currentPageIndex = 0; JSFunction? _pageChangeListener;
MyWebAdapter({ this.onStatusChanged, this.onDocumentInfo, });
@override Future<void> onInstanceLoaded( NutrientWebInstance instance) async { await super.onInstanceLoaded(instance);
final pageCount = instance.totalPageCount ?? instance.pageCount ?? 0; _currentPageIndex = instance.viewState.currentPageIndex;
onDocumentInfo?.call( pageCount: pageCount, currentPage: _currentPageIndex, title: 'Web Document', isReady: true, ); markReady(); onStatusChanged?.call('Document loaded via JS interop');
// Listen for page changes. _pageChangeListener = ((JSAny event) { _currentPageIndex = instance.viewState.currentPageIndex; onDocumentInfo?.call( currentPage: _currentPageIndex); }).toJS; instance.addEventListener( 'viewState.currentPageIndex.change', _pageChangeListener!); }
@override Future<int> getPageCount() async => instance?.totalPageCount ?? instance?.pageCount ?? 0;
@override Future<int> getCurrentPageIndex() async => _currentPageIndex;
@override Future<void> setPageIndex(int pageIndex) async { _currentPageIndex = pageIndex; final inst = instance; if (inst != null) { final currentViewState = inst.viewState; final updates = {'currentPageIndex': pageIndex}.jsify()!; final newViewState = currentViewState.merge(updates); inst.setViewState(newViewState as JSAny); } }
@override Future<String?> getDocumentTitle() async => 'Web Document';
@override Future<void> dispose() async { final inst = instance; if (inst != null && _pageChangeListener != null) { inst.removeEventListener( 'viewState.currentPageIndex.change', _pageChangeListener!); } _pageChangeListener = null; await super.dispose(); }}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
StatefulWidget.initState()creates the adapter via the platform factory function.- The adapter is stored as the controller interface type.
NutrientViewreceives the adapter cast toNutrientPlatformAdapter.- The platform view loads and adapter lifecycle methods fire.
- The document loads, and the adapter calls
onDocumentInfo(isReady: true). - The adapter calls
markReady()to signal that controller methods are safe to call. - 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.
import 'package:flutter/foundation.dart';import 'package:nutrient_flutter_ios/src/ios_platform_adapter.dart';import 'package:nutrient_flutter_ios/src/bindings/nutrient_ios_bindings.dart' as ios;import 'package:objective_c/objective_c.dart' as objc;
class EventIOSAdapter extends IOSAdapter { ios.PSPDFViewControllerDelegate? _delegate;
void _log(String message) => debugPrint('[Events-iOS] $message');
@override Future<void> onViewControllerReady( ios.PSPDFViewController viewController) async { _delegate = ios.PSPDFViewControllerDelegate$Builder .implementAsListener( pdfViewController_didChangeDocument: (ios.PSPDFViewController controller, ios.PSPDFDocument? document) { if (document != null) { _log('Document changed: ' '${document.pageCount} pages'); } }, pdfViewController_didConfigurePageView_forPageAtIndex: (controller, pageView, pageIndex) { _log('Page changed to $pageIndex'); }, pdfViewController_didSelectAnnotations_onPageView: (ios.PSPDFViewController controller, objc.NSArray annotations, ios.PSPDFPageView pageView) { _log('Selected ' '${annotations.count} annotation(s)'); }, pdfViewController_didDeselectAnnotations_onPageView: (ios.PSPDFViewController controller, objc.NSArray annotations, ios.PSPDFPageView pageView) { _log('Deselected ' '${annotations.count} annotation(s)'); }, pdfViewController_didChangeViewMode: (ios.PSPDFViewController controller, ios.PSPDFViewMode viewMode) { _log('View mode changed: $viewMode'); }, ); viewController.delegate = _delegate;
// Check for already-loaded document. final document = viewController.document; if (document != null) { _log('Initial document: ' '${document.pageCount} pages'); } }
@override Future<void> onViewControllerDetached() async { _delegate = null; }}For more information, refer to the iOS controller states guide.
import 'dart:js_interop';import 'package:flutter/foundation.dart';import 'package:nutrient_flutter_web/nutrient_flutter_web.dart';
class EventWebAdapter extends NutrientWebAdapter { JSFunction? _pageChangeListener; JSFunction? _zoomChangeListener; JSFunction? _annotationCreateListener; JSFunction? _annotationUpdateListener; JSFunction? _annotationDeleteListener;
void _log(String message) => debugPrint('[Events-Web] $message');
@override Future<void> onInstanceLoaded( NutrientWebInstance instance) async { await super.onInstanceLoaded(instance);
// Page change events. _pageChangeListener = ((JSAny event) { final pageIndex = instance.viewState.currentPageIndex; _log('Page changed to $pageIndex'); }).toJS; instance.addEventListener( 'viewState.currentPageIndex.change', _pageChangeListener!);
// Zoom change events. _zoomChangeListener = ((JSAny event) { final zoom = instance.currentZoomLevel; _log('Zoom: ${zoom.toStringAsFixed(2)}x'); }).toJS; instance.addEventListener( 'viewState.zoom.change', _zoomChangeListener!);
// Annotation events. _annotationCreateListener = ((JSAny event) { _log('Annotation created'); }).toJS; instance.addEventListener( 'annotations.create', _annotationCreateListener!);
_annotationUpdateListener = ((JSAny event) { _log('Annotation updated'); }).toJS; instance.addEventListener( 'annotations.update', _annotationUpdateListener!);
_annotationDeleteListener = ((JSAny event) { _log('Annotation deleted'); }).toJS; instance.addEventListener( 'annotations.delete', _annotationDeleteListener!); }
@override Future<void> dispose() async { final inst = instance; if (inst != null) { if (_pageChangeListener != null) { inst.removeEventListener( 'viewState.currentPageIndex.change', _pageChangeListener!); } if (_zoomChangeListener != null) { inst.removeEventListener( 'viewState.zoom.change', _zoomChangeListener!); } if (_annotationCreateListener != null) { inst.removeEventListener( 'annotations.create', _annotationCreateListener!); } if (_annotationUpdateListener != null) { inst.removeEventListener( 'annotations.update', _annotationUpdateListener!); } if (_annotationDeleteListener != null) { inst.removeEventListener( 'annotations.delete', _annotationDeleteListener!); } }
_pageChangeListener = null; _zoomChangeListener = null; _annotationCreateListener = null; _annotationUpdateListener = null; _annotationDeleteListener = null;
await super.dispose(); }}For more information, refer to the Web 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.
import 'package:nutrient_flutter_ios/src/ios_platform_adapter.dart';import 'package:nutrient_flutter_ios/src/bindings/nutrient_ios_bindings.dart' as ios;import 'package:objective_c/objective_c.dart' as objc;
class UIIOSAdapter extends IOSAdapter { @override void configureView( NutrientViewHandle handle, ios.PSPDFConfigurationBuilder builder, ) { super.configureView(handle, builder);
builder.thumbnailBarMode = ios.PSPDFThumbnailBarMode .PSPDFThumbnailBarModeScrollable; builder.searchMode = ios.PSPDFSearchMode.PSPDFSearchModeModal; builder.documentLabelEnabled = ios.PSPDFAdaptiveConditional .PSPDFAdaptiveConditionalYES; builder.userInterfaceViewMode = ios.PSPDFUserInterfaceViewMode .PSPDFUserInterfaceViewModeAutomatic; }
@override Future<void> onViewControllerReady( ios.PSPDFViewController viewController) async { // Customize the right bar button items. final navigationItem = viewController.navigationItem;
final rightItems = objc.NSMutableArray.array(); rightItems .addObject(viewController.thumbnailsButtonItem); rightItems.addObject(viewController.searchButtonItem); rightItems .addObject(viewController.bookmarkButtonItem); rightItems .addObject(viewController.activityButtonItem);
navigationItem .setRightBarButtonItems_forViewMode_animated( rightItems, viewMode: ios.PSPDFViewMode.PSPDFViewModeDocument, animated: false, ); }
@override Future<void> onViewControllerDetached() async {}}For more information, refer to the iOS main toolbar guide.
import 'package:nutrient_flutter_web/nutrient_flutter_web.dart';
class UIWebAdapter extends NutrientWebAdapter { @override Future<void> configureLoad( NutrientViewHandle handle, Map<String, dynamic> config, ) async { await super.configureLoad(handle, config);
config['theme'] = 'AUTO'; config['toolbarPlacement'] = 'TOP';
config['toolbarItems'] = [ {'type': 'sidebar-thumbnails'}, {'type': 'sidebar-document-outline'}, {'type': 'sidebar-annotations'}, {'type': 'sidebar-bookmarks'}, {'type': 'spacer'}, {'type': 'pager'}, {'type': 'zoom-out'}, {'type': 'zoom-in'}, {'type': 'zoom-mode'}, {'type': 'spacer'}, {'type': 'highlighter'}, {'type': 'text-highlighter'}, {'type': 'ink'}, {'type': 'ink-eraser'}, {'type': 'signature'}, {'type': 'note'}, {'type': 'text'}, {'type': 'line'}, {'type': 'arrow'}, {'type': 'rectangle'}, {'type': 'ellipse'}, {'type': 'spacer'}, {'type': 'print'}, {'type': 'search'}, {'type': 'export-pdf'}, ];
config['enableAnnotationToolbar'] = true; config['enableHistory'] = true; config['enableClipboardActions'] = true; config['enableTextSelection'] = true; config['enablePrinting'] = true; config['enableDownload'] = true; }
@override Future<void> onInstanceLoaded( NutrientWebInstance instance) async { await super.onInstanceLoaded(instance); }
@override Future<void> dispose() async { await super.dispose(); }}For more information, refer to the Web toolbar customization 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:
@overrideFuture<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:
@overrideFuture<void> onFragmentDetached() async { if (_documentListener != null && _pdfFragment != null) { _pdfFragment!.removeDocumentListener(_documentListener!); } _documentListener = null; _pdfFragment = null; _document = null;}Next steps
- Overview — Understand the adapter architecture
- Getting started — Installation and setup
- Platform imports — Handle platform-specific imports