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

Platform adapters give you direct access to native SDKs, but each platform uses different language bindings:

  • Android — JNI.
  • iOS — Objective-C FFI.
  • Web — JavaScript interop.

These bindings can’t coexist in one compilation target. For example, if your Android adapter imports JNI types, that import fails when you build for iOS or Web.

This guide shows you how to structure your code so each build includes only the bindings for its target platform. It also shows you how to register the resulting adapter with NutrientDocumentView.

The problem

Each platform adapter imports the bindings package for its target platform:

PlatformBinding typeImport
AndroidJNIpackage:nutrient_flutter_android/nutrient_flutter_android.dart
iOSObjective-C FFIpackage:nutrient_flutter_ios/nutrient_flutter_ios.dart
WebJS interoppackage:nutrient_flutter_web/nutrient_flutter_web.dart

These imports are mutually exclusive. The JNI package doesn’t exist when you compile for iOS, the Objective-C package doesn’t exist when you compile for Android, and dart:js_interop is only available on Web. If you import all three adapters in the same file, the build fails on every platform.

To fix this, isolate each adapter in its own file and use a factory to select the correct adapter at build time. The factory returns your shared controller interface, such as MyController in the getting started guide. Register that interface once with Nutrient.addAdapterClass<MyController>().

Platform checks for Android and iOS

If your app only targets Android and iOS, use runtime platform checks with dart:io. Each adapter lives in its own file, so the Dart compiler doesn’t encounter bindings for the wrong platform:

import 'dart:io';
import 'package:nutrient_flutter/bindings.dart';
// Each file only imports its own platform's bindings.
import 'adapters/my_android_adapter.dart';
import 'adapters/my_controller.dart';
import 'adapters/my_ios_adapter.dart';
MyController createAdapter() {
if (Platform.isAndroid) return MyAndroidAdapter();
if (Platform.isIOS) return MyIOSAdapter();
throw UnsupportedError('Platform not supported');
}
// Register the controller type once at startup.
void registerAdapter() {
Nutrient.addAdapterClass<MyController>(createAdapter);
}

This works because my_android_adapter.dart only imports JNI types and my_ios_adapter.dart only imports Objective-C types. The Dart compiler tree-shakes the unused platform at build time.

Important: This approach doesn’t work when targeting Web. The dart:io library isn’t available on the Web platform. If you need to support Web, use the conditional imports approach below.

Conditional imports

When your app targets Android, iOS, and Web, use Dart conditional imports to resolve the correct adapter at compile time. This is the standard Dart mechanism for platform-specific code.

How it works

Dart conditional export syntax selects which file to include based on platform library availability:

export 'my_adapter_stub.dart'
if (dart.library.io) 'my_adapter_native.dart'
if (dart.library.js_interop) 'my_adapter_web.dart';

The Dart compiler evaluates these conditions at build time:

  • dart.library.io is available on Android and iOS, so it selects my_adapter_native.dart.
  • dart.library.js_interop is available on Web, so it selects my_adapter_web.dart.
  • Neither condition selects the stub fallback.

Each build includes only one file. The compiler doesn’t compile the other files or their platform-specific imports.

File structure

Use this file structure to isolate adapter implementations and expose one shared entry point:

lib/
├── main.dart
└── adapters/
├── my_adapter.dart # Entry point with conditional exports
├── my_adapter_stub.dart # Fallback for unsupported platforms
├── my_adapter_native.dart # Android/iOS factory (uses dart:io)
├── my_adapter_web.dart # Web factory
├── my_controller.dart # Shared controller interface (no platform imports)
├── my_android_adapter.dart # Android adapter (imports JNI bindings)
├── my_ios_adapter.dart # iOS adapter (imports ObjC bindings)
└── my_web_adapter.dart # Web adapter (imports JS interop)

Each platform adapter file imports only its own platform bindings. The factory files create the correct adapter and return it as the shared controller interface type.

Step one: Create the entry point

The entry point file reexports the controller interface and conditionally exports the correct factory function:

lib/adapters/my_adapter.dart
library;
export 'my_controller.dart';
export 'my_adapter_stub.dart'
if (dart.library.io) 'my_adapter_native.dart'
if (dart.library.js_interop) 'my_adapter_web.dart';

Consumers import this single file. The conditional export resolves to the correct factory at compile time.

Step two: Add the stub fallback

The stub defines the same createMyAdapter() signature as the other factory files. It returns null or throws on unsupported platforms:

lib/adapters/my_adapter_stub.dart
import 'my_controller.dart';
MyController? createMyAdapter() => null;

Step three: Add the native factory for Android and iOS

This file is selected when dart.library.io is available. It uses dart:io for runtime platform detection:

lib/adapters/my_adapter_native.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;
}

Step four: Add the web factory

This file is selected when dart.library.js_interop is available:

lib/adapters/my_adapter_web.dart
import 'my_controller.dart';
import 'my_web_adapter.dart';
MyController? createMyAdapter() => MyWebAdapter();

Step five: Register and use the adapter

Import the entry point file. The createMyAdapter() function resolves to the correct factory at compile time, so you can register the controller type once and select it with the NutrientDocumentView type parameter:

import 'package:flutter/material.dart';
import 'package:nutrient_flutter/bindings.dart';
// Conditional import resolves to the correct platform factory.
import 'adapters/my_adapter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Nutrient.initialize();
Nutrient.addAdapterClass<MyController>(() => createMyAdapter()!);
runApp(const MyApp());
}
class PdfViewerPage extends StatelessWidget {
final String documentPath;
const PdfViewerPage({super.key, required this.documentPath});
@override
Widget build(BuildContext context) {
return NutrientDocumentView<MyController>(documentPath: documentPath);
}
}

The factory builds a new adapter for each view, and the view owns the adapter lifecycle. If you want to hold the instance yourself, call createMyAdapter() directly and pass it as NutrientDocumentView.adapter. For more examples, refer to the usage patterns guide.

Key points

Keep these principles in mind when you structure platform-specific code:

  • Keep each platform adapter in its own file to isolate platform-specific imports.
  • Keep platform imports out of the shared controller interface. The interface is the registry key and the widget’s type parameter.
  • Define the same createMyAdapter() signature in every factory file so the calling code stays platform-agnostic.
  • Return null from the factory on unsupported platforms, and handle that case when you register or attach the adapter.
  • Use dart:io platform checks for native-only apps. Add conditional imports when you also target Web.

Next steps

Continue with this guide: