This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/flutter/platform-adapters/usage.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Usage patterns

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(...)`.
}
}

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:

lib/adapters/my_controller.dart
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);
}

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();
}
}

Best practices

Follow these practices when you implement platform adapters:

  • Call super where the base implements the hook. Call super in Android onFragmentReady and configureFragment, iOS configureView, and Web onInstanceLoaded and configureLoad. iOS onViewControllerReady and onViewControllerDetached are abstract, so implement them without super.
  • Null-check native handles. Native handles are null until the matching lifecycle hook fires, so guard with ?..
  • Remove native listeners on detach. Detach DocumentListeners and cancel stream subscriptions in onFragmentDetached, onViewControllerDetached, or dispose so 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: