This guide shows common platform adapter patterns in Nutrient Flutter SDK. Each example uses a custom adapter — the controller behind a NutrientDocumentView — to reach the native SDK.
The examples build on the MyController setup from the getting started guide. That setup includes a cross-platform interface, per-platform adapters, and a conditional-import factory. This guide adds the methods and lifecycle hooks you need for common adapter use cases.
Basic usage
A basic viewer doesn’t need an adapter — NutrientDocumentView builds the SDK’s default controller. Start with this pattern before you add a custom adapter:
import 'package:flutter/material.dart';import 'package:nutrient_flutter/bindings.dart';
class BasicViewer extends StatelessWidget { final String documentPath;
const BasicViewer({super.key, required this.documentPath});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('PDF Viewer')), body: NutrientDocumentView(documentPath: documentPath), ); }}Configure the viewer
Each platform adapter exposes a configuration hook that runs before the native viewer is built. Override this hook to customize the native configuration:
class MyAndroidAdapter extends AndroidAdapter implements MyController { @override Future<void> configureFragment( NutrientViewHandle handle, PdfUiFragmentBuilder builder, Context context, ) async { await super.configureFragment(handle, builder, context); // Build a `PdfActivityConfiguration` and apply it with `builder.configuration(...)`. }}class MyIOSAdapter extends IOSAdapter implements MyController { @override void configureView( NutrientViewHandle handle, PSPDFConfigurationBuilder builder, ) { super.configureView(handle, builder); // Set properties on the `PSPDFConfigurationBuilder`. `configureView` is // synchronous, unlike the Android and Web configuration hooks. }}class MyWebAdapter extends NutrientWebAdapter implements MyController { @override Future<void> configureLoad( NutrientViewHandle handle, Map<String, dynamic> config, ) async { await super.configureLoad(handle, config); // Mutate the `NutrientViewer.load()` config map, e.g. `config['theme'] = 'AUTO'`. }}For the full set of native configuration properties on each platform, refer to the SDK reference guide.
Implement controller operations
Add methods to your controller interface, and then back them with native calls in each adapter. Use this pattern to expose document navigation and metadata that the cross-platform API doesn’t cover yet:
import 'package:nutrient_flutter/bindings.dart';
abstract class MyController implements NutrientController { Future<int> getPageCount(); Future<int> getCurrentPageIndex(); Future<void> goToPage(int pageIndex); Future<String?> getDocumentTitle();}class MyAndroidAdapter extends AndroidAdapter implements MyController { @override Future<int> getPageCount() async => nativePdfDocument?.getPageCount() ?? 0;
@override Future<int> getCurrentPageIndex() async => nativePdfFragment?.pageIndex ?? 0;
@override Future<void> goToPage(int pageIndex) async { // The (index, animated) overload of `setPageIndex`. nativePdfFragment?.setPageIndex(pageIndex, true); }
@override Future<String?> getDocumentTitle() async => nativePdfDocument?.getTitle()?.toDartString(releaseOriginal: true);}class MyIOSAdapter extends IOSAdapter implements MyController { @override Future<void> onViewControllerReady(PSPDFViewController viewController) async { // Native handles become available here. }
@override Future<int> getPageCount() async => nativeDocument?.pageCount ?? 0;
@override Future<int> getCurrentPageIndex() async => nativeViewController?.pageIndex ?? 0;
@override Future<void> goToPage(int pageIndex) async { nativeViewController?.setPageIndex_animated(pageIndex, animated: true); }
@override Future<String?> getDocumentTitle() async => nativeDocument?.title?.toDartString();}class MyWebAdapter extends NutrientWebAdapter implements MyController { @override Future<int> getPageCount() async => instance?.totalPageCount.toInt() ?? 0;
@override Future<int> getCurrentPageIndex() async => currentPageIndex ?? 0;
@override Future<void> goToPage(int pageIndex) async => setCurrentPageIndex(pageIndex);
@override Future<String?> getDocumentTitle() async => 'Web Document';}Call these methods through the controller. The same code runs on every platform:
NutrientDocumentView<MyController>( documentPath: documentPath, onControllerReady: (controller) async { debugPrint('Title: ${await controller.getDocumentTitle()}'); debugPrint('Pages: ${await controller.getPageCount()}'); await controller.goToPage(2); },);Listen to native events
controller.events carries typed cross-platform events. For native or platform-only events that the cross-platform stream doesn’t expose, wire a native listener in the adapter lifecycle hook.
Add a DocumentListener to the fragment for callbacks like onDocumentClick that aren’t on the typed stream:
class MyAndroidAdapter extends AndroidAdapter implements MyController { DocumentListener? _docListener;
@override Future<void> onFragmentReady(PdfFragment pdfFragment) async { await super.onFragmentReady(pdfFragment); _docListener = DocumentListener.implement( $DocumentListener( onDocumentClick: () { debugPrint('Document clicked'); return false; // Don't consume the tap. }, onPageChanged: (_, pageIndex) => debugPrint('Page: $pageIndex'), onDocumentLoaded: (document) => debugPrint('Loaded ${document.getPageCount()} pages'), onDocumentLoadFailed: (_) {}, onDocumentSave: (_, __) => true, onDocumentSaved: (_) {}, onDocumentSaveFailed: (_, __) {}, onDocumentSaveCancelled: (_) {}, onPageClick: (_, __, ___, ____, _____) => false, onDocumentZoomed: (_, __, ___) {}, onPageUpdated: (_, __) {}, ), ); pdfFragment.addDocumentListener(_docListener!); }
@override Future<void> onFragmentDetached() async { final fragment = nativePdfFragment; if (_docListener != null && fragment != null) { fragment.removeDocumentListener(_docListener!); } _docListener = null; await super.onFragmentDetached(); }}Subscribe to iosEvents for iOS-only events such as view-mode changes and user-interface visibility:
import 'dart:async';
class MyIOSAdapter extends IOSAdapter implements MyController { StreamSubscription<IOSNutrientEvent>? _iosEventSub;
@override Future<void> onViewControllerReady(PSPDFViewController viewController) async { _iosEventSub = iosEvents.listen((event) { switch (event) { case IOSViewModeChangedEvent(:final viewMode): debugPrint('View mode changed: $viewMode'); case IOSUserInterfaceShownEvent(): debugPrint('User interface shown'); default: break; } }); }
@override Future<void> onViewControllerDetached() async { await _iosEventSub?.cancel(); _iosEventSub = null; }}Subscribe to webEvents for raw Web SDK events such as zoom changes:
import 'dart:async';
class MyWebAdapter extends NutrientWebAdapter implements MyController { StreamSubscription<NutrientWebEventData>? _webEventSub;
@override Future<void> onInstanceLoaded(Instance instance) async { await super.onInstanceLoaded(instance); _webEventSub = webEvents.listen((event) { if (event.type == 'viewState.zoom.change') { debugPrint('Zoom: ${instance.currentZoomLevel.toStringAsFixed(2)}x'); } }); }
@override Future<void> dispose() async { await _webEventSub?.cancel(); _webEventSub = null; await super.dispose(); }}Best practices
Follow these practices when you implement platform adapters:
- Call
superwhere the base implements the hook. Callsuperin AndroidonFragmentReadyandconfigureFragment, iOSconfigureView, and WebonInstanceLoadedandconfigureLoad. iOSonViewControllerReadyandonViewControllerDetachedare abstract, so implement them withoutsuper. - Null-check native handles. Native handles are
nulluntil the matching lifecycle hook fires, so guard with?.. - Remove native listeners on detach. Detach
DocumentListeners and cancel stream subscriptions inonFragmentDetached,onViewControllerDetached, ordisposeso they don’t leak across views. - Keep the interface platform-agnostic. The controller interface is the registry key and the view’s type parameter. It must not import platform packages. For details, refer to the platform imports guide.
Next steps
Continue with these guides:
- Refer to the getting started guide to create your first adapter.
- Refer to the platform imports guide to handle platform-specific imports.
- Refer to the SDK reference guide for native configuration and API references.