Pigeon usage at Nutrient: Bridging native SDKs to Flutter
Table of contents
At Nutrient, we deliver a high-performance PDF SDK that powers PDF viewing, annotation, form filling, digital signatures, and real-time collaboration across multiple platforms. Our Flutter SDK wraps these native implementations, providing Flutter developers with the same powerful PDF capabilities through a unified API. Since our SDK bridges complex native functionality to Flutter, tight, type-safe interoperability is essential. Pigeon enables us to expose native functionality with minimal boilerplate while maintaining strong type safety across all platforms.
This post will explore:
- What Pigeon is — Understanding the tool and why we chose it
- Before Pigeon — The challenges we faced with raw method channels
- After Pigeon — The improvements in code efficiency, crash rates, and development speed
- What’s next — How Google I/O 2025 announcements (FFIgen, JNIgen, unified interop) will shape our roadmap
What Pigeon is, and why we chose it
Pigeon(opens in a new tab) is a code generation tool that creates type-safe communication between Flutter and host platforms. Instead of manually writing platform channel code, you define your API once in Dart, and Pigeon generates the corresponding native code for iOS, Android, and Flutter.
The Flutter platform channel challenge
Flutter apps often need to access platform-specific functionality that isn’t available in Dart. The traditional approach uses method channels — a message-passing system between Flutter and native code. However, this approach has significant drawbacks:
- No type safety — Data is passed as generic
Map<String, dynamic>objects - Runtime errors — Type mismatches only surface when the code runs
- Manual serialization — Developers must handle data conversion on both sides
- Maintenance overhead — Changes require updates in multiple places
Why Pigeon made sense for Nutrient
When we evaluated solutions for our Flutter SDK, Pigeon stood out for several reasons:
Type safety — Pigeon generates strongly typed interfaces, catching errors at compile time rather than runtime. For a complex PDF SDK with hundreds of methods, this was crucial.
Reduced boilerplate — Instead of writing serialization code for each platform, we define our API once and let Pigeon handle the rest. This dramatically reduced our codebase size.
Consistency — All our platform bridges follow the same patterns, making the code easier to understand and maintain across iOS and Android.
Asynchronous support — Pigeon supports both synchronous and asynchronous operations, which is essential for PDF operations that can be time-consuming.
Active development — As a Google-backed project, Pigeon receives regular updates and has strong community support.
Before Pigeon: Manual platform channel implementation
Before we started using Pigeon, here’s how we implemented processAnnotations with manual platform channels:
// From `nutrient_flutter_method_channel.dart`.Future<bool?> processAnnotations( AnnotationType type, AnnotationProcessingMode processingMode, String destinationPath,) async => methodChannel.invokeMethod('processAnnotations', <String, String>{ 'type': type.fullName, 'processingMode': processingMode.name, 'destinationPath': destinationPath });There were a few problems with this, namely the manual parameter serialization into a <String, String> map, explicit enum-to-string conversion, and string-based method names.
After Pigeon: The same API, simplified
Here’s the same processAnnotations method using Pigeon:
// From `nutrient_flutter_api_impl.dart`.Future<bool?> processAnnotations( AnnotationType type, AnnotationProcessingMode processingMode, String destinationPath) { return _nutrientApi.processAnnotations( type, processingMode, destinationPath );}What changed? Now there’s a direct API call with native parameter types, no manual serialization, and type-safe method invocation.
But the real difference comes from manual map creation and enum conversion vs. direct, type-safe parameter passing.
Why we like Pigeon
After using Pigeon in production for our PDF SDK, these are the benefits that matter most to us:
1. Single source of truth — API definitions live in one Dart file. When we change a method signature, the compiler immediately tells us what breaks across iOS, Android, and Flutter, which means no more discovering API mismatches at runtime.
2. Developer velocity — Compare the processAnnotations examples above: Eight lines of manual serialization became three lines of direct method calls. Multiply this across 200+ API methods, and the productivity gain is substantial.
3. Bulletproof type safety — No more as Map<String, dynamic> casting or enum-to-string conversions. Pigeon’s generated code handles type safety automatically, eliminating an entire class of runtime crashes.
4. Consistent patterns — Every API follows the same generated structure. New team members don’t need to learn our custom serialization patterns; they just work with clean, predictable interfaces.
5. Future-proof architecture — As our SDK evolves, Pigeon scales with us. Adding new methods or changing existing ones is now a matter of updating one definition file rather than three platform implementations.
How Nutrient uses Pigeon in practice
At Nutrient, our Flutter SDK is built on top of our mature native iOS, Android, and Web SDKs. Pigeon serves as the primary bridge between Flutter and our native platforms, handling the vast majority of our API surface. Check out our Flutter customization guide(opens in a new tab) for more details on how we use Pigeon to connect our native SDKs to Flutter.
Explore our Flutter customization guide to see how we use Pigeon to bridge native functionality and build type-safe platform integrations.
Limitations with inheritance and abstract classes
Pigeon has significant constraints when it comes to object-oriented programming patterns that are common in native SDKs.
What’s not supported:
- Complex inheritance hierarchies — Data class inheritance isn’t preserved in generated code(opens in a new tab), which means inherited properties are lost during code generation.
- Abstract classes as return types — Pigeon fails with “Unknown type” errors(opens in a new tab) when abstract classes are used as return values.
- Polymorphic APIs — It cannot pass different subclass types through the same interface.
What’s partially supported:
- Basic inheritance with empty sealed parent classes — This is only supported in Swift, Kotlin, and Dart generators, and only for empty parent classes.
- Interface-like patterns — This can be achieved through composition rather than inheritance. However, this limitation affects many native SDKs that use object-oriented designs. For example, annotation systems where
TextAnnotation,InkAnnotation, andShapeAnnotationall inherit from a baseAnnotationclass cannot be directly modeled in Pigeon.
The future: FFIgen and JNIgen
In the Google announcement, “Flutter’s path towards seamless interop”(opens in a new tab), the Flutter team highlighted FFIgen and JNIgen as the next generation of native interoperability tools. Unlike Pigeon, which uses method channels, these tools generate direct bindings with better performance characteristics.
- FFIgen — Creates Dart FFI bindings for C/Objective-C/Swift APIs (stable, v20.x)
- JNIgen — Generates JNI bindings for Java/Kotlin APIs (maturing, v0.15.x)
Benefits over Pigeon:
- Synchronous API calls (no forced async)
- Better tree-shaking support
- More data can live in the platform layer
Current status (November 2025):
- FFIgen is stable, with more than 1 million downloads on pub.dev.
- JNIgen is pre-1.0, but it’s actively maintained and production-ready for many use cases.
- Platform view support is possible; examples exist of PDF viewers using JNIgen with platform views(opens in a new tab).
- Still requires familiarity with native platforms and some wrapper implementations.
At Nutrient, we’re in the early phases of experimenting with FFIgen and JNIgen to explore a bindings-based approach for our Flutter SDK. While Pigeon continues to serve us well, direct native bindings could offer performance improvements and help us overcome the inheritance limitations we currently work around.
Conclusion: Pigeon at Nutrient
For Nutrient Flutter SDK, Pigeon has been a gamechanger despite its limitations. The benefits — type safety, reduced boilerplate, faster development cycles, and fewer runtime errors — far outweigh the challenges. While we occasionally need manual workarounds for complex inheritance hierarchies, Pigeon handles the vast majority of our platform bridging needs elegantly.
The upcoming FFIgen and JNIgen tools promise even better performance and flexibility, which could help us overcome current limitations. However, Pigeon’s simplicity and battle-tested reliability make it an excellent choice for teams building production Flutter plugins today.
If you’re considering Pigeon for your Flutter plugin:
- It excels at straightforward API bridging with simple data types.
- Plan for manual handling of inheritance-heavy APIs.
- The productivity gains from type-safe code generation are substantial.
- The Flutter team’s continued investment in native interoperability tools means the ecosystem will only get better.
At Nutrient, Pigeon remains an essential tool in our Flutter SDK development toolkit, enabling us to deliver a consistent, reliable document processing experience across iOS and Android while maintaining our development velocity.
Experience Nutrient Flutter SDK with seamless native interoperability. Add PDF viewing, annotation, and collaboration to your Flutter app.