Platform imports
Platform adapters give you direct access to native SDKs, but each platform uses different language bindings — JNI on Android, Objective-C FFI on iOS, and JavaScript interop on Web. These bindings cannot coexist in a single compilation target. If your Android adapter imports JNI types, that import will fail when building for iOS or Web, and vice versa.
This guide explains how to structure your code so that only the correct platform’s bindings are included in each build.
The problem
Each platform adapter imports bindings that only exist on its target platform:
| Platform | Binding type | Import |
|---|---|---|
| Android | JNI | package:nutrient_flutter_android/src/bindings/nutrient_android_sdk_bindings.dart |
| iOS | Objective-C FFI | package:nutrient_flutter_ios/src/bindings/nutrient_ios_bindings.dart |
| Web | JS interop | package:nutrient_flutter_web/nutrient_flutter_web.dart |
These imports are mutually exclusive. The JNI package doesn’t exist when compiling for iOS, the Objective-C package doesn’t exist when compiling for Android, and dart:js_interop is only available on Web. If you import all three adapters in the same file, the build will fail on every platform.
The solution is to isolate each adapter in its own file and use a factory pattern to select the correct one at build time.
Platform checks (Android and iOS only)
If your app only targets Android and iOS, the simplest approach is runtime platform checks with dart:io. Each adapter lives in its own file, so the Dart compiler never encounters bindings for the wrong platform:
import 'dart:io';import 'package:flutter/material.dart';import 'package:nutrient_flutter/nutrient_flutter.dart';
// Each file only imports its own platform's bindings.import 'adapters/my_android_adapter.dart';import 'adapters/my_ios_adapter.dart';
NutrientPlatformAdapter _createAdapter() { if (Platform.isAndroid) { return MyAndroidAdapter(); } else if (Platform.isIOS) { return MyIOSAdapter(); } throw UnsupportedError('Platform not supported');}
class PdfViewerPage extends StatelessWidget { final String documentPath;
const PdfViewerPage( {super.key, required this.documentPath});
@override Widget build(BuildContext context) { return NutrientView( documentPath: documentPath, adapter: _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 (recommended)
When your app targets Android, iOS, and Web, use Dart’s conditional imports to resolve the correct adapter at compile time. This is the standard Dart mechanism for platform-specific code.
How it works
Dart’s conditional export syntax selects which file to include based on the availability of platform libraries:
export 'adapters_stub.dart' if (dart.library.io) 'adapters_native.dart' if (dart.library.js_interop) 'adapters_web.dart';The Dart compiler evaluates these conditions at build time:
dart.library.iois available on Android and iOS — selectsadapters_native.dart.dart.library.js_interopis available on Web — selectsadapters_web.dart.- If neither is available, the stub fallback is used.
Only one file is included per build. The other files and their platform-specific imports are never compiled.
File structure
lib/├── main.dart└── adapters/ ├── adapters.dart # Entry point with conditional exports ├── adapters_stub.dart # Fallback for unsupported platforms ├── adapters_native.dart # Android/iOS factory (uses dart:io) ├── adapters_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 only imports its own platform’s bindings. The factory files create the correct adapter and return it as the shared controller interface type.
Step 1: Entry point
The entry point file reexports the controller interface and conditionally exports the correct factory function:
library;
export 'my_controller.dart';export 'adapters_stub.dart' if (dart.library.io) 'adapters_native.dart' if (dart.library.js_interop) 'adapters_web.dart';Consumers import this single file. The conditional export resolves to the correct factory at compile time.
Step 2: Stub fallback
The stub provides a default implementation for unsupported platforms. It defines the same createAdapter() function signature as the other factory files, but returns null:
import 'my_controller.dart';
MyController? createAdapter({ StatusChangedCallback? onStatusChanged, DocumentInfoCallback? onDocumentInfo,}) { return null;}Step 3: Native factory (Android and iOS)
This file is selected when dart.library.io is available. It uses dart:io for runtime platform detection and creates the correct adapter:
import 'dart:io';
import 'my_android_adapter.dart';import 'my_ios_adapter.dart';import 'my_controller.dart';
MyController? createAdapter({ StatusChangedCallback? onStatusChanged, DocumentInfoCallback? onDocumentInfo,}) { if (Platform.isAndroid) { return MyAndroidAdapter( onStatusChanged: onStatusChanged, onDocumentInfo: onDocumentInfo, ); } else if (Platform.isIOS) { return MyIOSAdapter( onStatusChanged: onStatusChanged, onDocumentInfo: onDocumentInfo, ); } return null;}Step 4: Web factory
This file is selected when dart.library.js_interop is available:
import 'my_web_adapter.dart';import 'my_controller.dart';
MyController? createAdapter({ StatusChangedCallback? onStatusChanged, DocumentInfoCallback? onDocumentInfo,}) { return MyWebAdapter( onStatusChanged: onStatusChanged, onDocumentInfo: onDocumentInfo, );}Step 5: Usage
Import the entry point file. The createAdapter() function is available regardless of platform — Dart resolves it to the correct factory at compile time:
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 PdfViewerPage extends StatelessWidget { final String documentPath;
const PdfViewerPage( {super.key, required this.documentPath});
@override Widget build(BuildContext context) { final controller = createAdapter(); return NutrientView( documentPath: documentPath, adapter: controller as NutrientPlatformAdapter?, ); }}All three factory files define createAdapter() with the same signature, so the calling code works identically on every platform.
Key points
Keep the following principles in mind when structuring platform-specific code:
- Keep each platform adapter in its own file to isolate platform-specific imports.
- The shared controller interface and callback types contain no platform imports.
- All factory files define the same function signature so the calling code is platform-agnostic.
- The
createAdapter()factory returnsnullon unsupported platforms — handle this in your UI. - For native-only apps,
dart:ioplatform checks are sufficient. Add conditional imports when you also target Web.
Next steps
- Usage patterns — See full examples