---
title: "Usage patterns"
canonical_url: "https://www.nutrient.io/guides/flutter/platform-adapters/usage/"
md_url: "https://www.nutrient.io/guides/flutter/platform-adapters/usage.md"
last_updated: "2026-05-20T19:49:34.815Z"
description: "Learn different usage patterns and scenarios for platform adapters in Nutrient Flutter SDK through practical examples covering configuration, controllers, event listeners, and UI customization."
---

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

## Example overview

| Example                                                   | Description                        | Key concepts                                                    |
| --------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------- |
| [Basic usage](#1-basic-usage)                             | Simplest PDF viewer implementation | `NutrientView`, initialization                                  |

| [Configuration examples](#2-configuration-examples)       | Platform-specific configuration    | Adapters, `configureFragment`, `configureView`, `configureLoad` |

| [Controller implementation](#3-controller-implementation) | Programmatic document control      | Custom controllers, typed interface, lifecycle                  |

| [Event listeners](#4-event-listeners)                     | Responding to document events      | Platform callbacks, lifecycle events                            |

| [UI customization](#5-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:

```dart

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:

### Android

```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 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](https://www.nutrient.io/guides/android/viewer.md).

### iOS

```dart

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](https://www.nutrient.io/guides/ios/getting-started/view-controller-configuration.md).

### Web

```dart

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](https://www.nutrient.io/guides/web/viewer.md) guide.

### Using adapters with NutrientView

```dart

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:

```dart

// 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.

### Android

```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;

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

```

### iOS

```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 '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;
  }
}

```

### Web

```dart

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

```dart

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.

### Android

```dart

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](https://www.nutrient.io/guides/android/events-and-notifications/events.md) guide.

### iOS

```dart

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](https://www.nutrient.io/guides/ios/customizing-the-interface/state-customization.md) guide.

### Web

```dart

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](https://www.nutrient.io/guides/web/events.md) guide.

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

## 5. UI customization

Customize toolbars and menus using platform-specific adapter hooks.

### Android

```dart

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](https://www.nutrient.io/guides/android/customizing-the-interface/customizing-menus.md) guide.

### iOS

```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 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](https://www.nutrient.io/guides/ios/user-interface/main-toolbar.md) guide.

### Web

```dart

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](https://www.nutrient.io/guides/web/user-interface/main-toolbar/) 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:

```dart

@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:

```dart

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

```

### 3. Implement controllers consistently

Implement the same interface on all platforms for consistency:

```dart

// 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:

```dart

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

```

## Next steps

- [Overview](https://www.nutrient.io/guides/flutter/platform-adapters.md) — Understand the adapter architecture

- [Getting started](https://www.nutrient.io/guides/flutter/platform-adapters/getting-started.md) — Installation and setup

- [Platform imports](https://www.nutrient.io/guides/flutter/platform-adapters/platform-imports.md) — Handle platform-specific imports
---

## Related pages

- [Android](/guides/flutter/platform-adapters/getting-started.md)
- [Platform Adapters](/guides/flutter/platform-adapters.md)
- [Native Sdk Reference](/guides/flutter/platform-adapters/native-sdk-reference.md)
- [Platform Imports](/guides/flutter/platform-adapters/platform-imports.md)

