Getting started with platform adapters
This guide shows you how to display a document with the cross-platform API and create a custom platform adapter when you need native access. To decide when to use an adapter, refer to the overview guide.
Prerequisites
Set up a working Nutrient Flutter project before you use platform adapters. For installation steps, supported SDK versions, and platform requirements, refer to the Flutter getting started guide.
Display a document
Most apps don’t need an adapter. Initialize the SDK once at startup, and then display a document with NutrientDocumentView. The widget builds the SDK’s default adapter and gives you a NutrientController in onControllerReady:
import 'package:flutter/material.dart';import 'package:nutrient_flutter/bindings.dart';
class ViewerPage extends StatelessWidget { final String documentPath;
const ViewerPage({super.key, required this.documentPath});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('PDF Viewer')), body: NutrientDocumentView( documentPath: documentPath, onControllerReady: (controller) async { final pageCount = await controller.document.getPageCount(); debugPrint('Loaded $pageCount pages'); controller.events.listen((event) => debugPrint('Event: $event')); }, ), ); }}Initialize the SDK before you show a viewer. Nutrient.initialize() handles licensing only:
// Pass license keys to remove the trial watermark; omit them for trial mode.await Nutrient.initialize( androidLicenseKey: 'YOUR_ANDROID_KEY', iosLicenseKey: 'YOUR_IOS_KEY', webLicenseKey: 'YOUR_WEB_KEY',);Use controller.document for document operations, including annotations, bookmarks, forms, saving and exporting, and page information. Use controller.events for the typed cross-platform event stream. These APIs work the same way on Android, iOS, and Web. The view owns the controller and disposes it for you.
Create a custom adapter
Create a custom adapter when you need native access or controller methods that the cross-platform API doesn’t provide. Define a cross-platform controller interface, implement it for each platform, and register the controller type once. Each NutrientDocumentView<T> builds a new adapter for the view and disposes it for you.
Step one: Define the controller interface
The interface acts as the registry key and the widget’s type parameter. It extends NutrientController and doesn’t include platform imports:
import 'package:nutrient_flutter/bindings.dart';
abstract class MyController implements NutrientController { /// Page count read directly from the native document handle. Future<int> nativePageCount();}Step two: Implement the adapter for each platform
Each implementation extends its platform base class, implements the shared interface, and reads the native handle inside the platform lifecycle hook.
import 'package:nutrient_flutter_android/nutrient_flutter_android.dart';
import 'my_controller.dart';
class MyAndroidAdapter extends AndroidAdapter implements MyController { // Override `onFragmentReady` (not `onPdfFragmentReady`) — the base wires the // event listeners and then calls this for your customization. @override Future<void> onFragmentReady(PdfFragment pdfFragment) async { await super.onFragmentReady(pdfFragment); // `nativePdfFragment`/`nativePdfDocument` are available here. }
@override Future<int> nativePageCount() async => nativePdfDocument?.getPageCount() ?? 0;}import 'package:nutrient_flutter_ios/nutrient_flutter_ios.dart';
import 'my_controller.dart';
class MyIOSAdapter extends IOSAdapter implements MyController { // `onViewControllerReady` is abstract on `IOSAdapter` — implement it without // calling super. @override Future<void> onViewControllerReady(PSPDFViewController viewController) async { // `nativeDocument`/`nativeViewController` are available here. }
@override Future<int> nativePageCount() async => nativeDocument?.pageCount ?? 0;}import 'package:nutrient_flutter_web/nutrient_flutter_web.dart';
import 'my_controller.dart';
class MyWebAdapter extends NutrientWebAdapter implements MyController { @override Future<void> onInstanceLoaded(Instance instance) async { await super.onInstanceLoaded(instance); // `instance` is the native Nutrient Web SDK instance. }
@override Future<int> nativePageCount() async => instance?.totalPageCount.toInt() ?? 0;}Step three: Add a conditional-import factory
The platform bindings can’t coexist in one build, so expose a single createMyAdapter() function through a conditional-import barrel. Your screens only import this barrel file:
export 'my_adapter_stub.dart' if (dart.library.io) 'my_adapter_native.dart' if (dart.library.js_interop) 'my_adapter_web.dart';import 'dart:io' show Platform;
import 'my_android_adapter.dart';import 'my_controller.dart';import 'my_ios_adapter.dart';
MyController? createMyAdapter() { if (Platform.isAndroid) return MyAndroidAdapter(); if (Platform.isIOS) return MyIOSAdapter(); return null;}The web factory (my_adapter_web.dart) returns MyWebAdapter(), and the stub (my_adapter_stub.dart) throws for unsupported targets. For the full file layout, refer to the platform imports guide.
Step four: Register the type and use it
Register the controller type once at startup. Then select it with the NutrientDocumentView type parameter:
import 'package:flutter/material.dart';import 'package:nutrient_flutter/bindings.dart';
import 'adapters/my_adapter.dart';import 'adapters/my_controller.dart';
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Nutrient.initialize(); Nutrient.addAdapterClass<MyController>(() => createMyAdapter()!); runApp(const MyApp());}NutrientDocumentView<MyController>( documentPath: documentPath, onControllerReady: (controller) async { // `controller` is your `MyController` — the adapter is the controller. final pages = await controller.nativePageCount(); debugPrint('Native page count: $pages'); },)If you use NutrientDocumentView<MyController> without registering MyController, the SDK throws a StateError that tells you to register the type or pass an adapter:.
Verify the installation
Run your app on each target platform to confirm that the adapter works:
# Androidflutter run -d android
# iOSflutter run -d ios
# Webflutter run -d chromeThe PDF viewer displays your document, and your adapter’s lifecycle hooks run on the matching platform.
Troubleshooting
Use this section to resolve common issues with platform imports and adapter registration.
Import errors when building for a different platform
If you see errors like Target of URI doesn’t exist or Dart library 'dart:io' is not available on this platform, an adapter file is importing bindings for the wrong platform. Keep each adapter in its own file and select between them with the conditional-import barrel. For more information, refer to the platform imports guide.
StateError about a missing adapter class
A NutrientDocumentView<MyController> throws a StateError when no builder is registered for that type. Call Nutrient.addAdapterClass<MyController>(() => createMyAdapter()!) once at startup, or pass a per-view adapter: instance instead.
Adapter lifecycle hooks aren’t called
Verify that the adapter uses the correct type for the current platform:
- Android — Override
onFragmentReady, notonPdfFragmentReady. The base class usesonPdfFragmentReadyto wire the event stream. - iOS — Implement
onViewControllerReady. - Web — Override
onInstanceLoaded.
Next steps
Continue with these guides:
- Refer to the usage patterns guide for native configuration, controller operations, and native events.
- Refer to the platform imports guide to handle platform-specific imports.