Flutter: When Cross-Platform Convenience Meets Low-Level Reality
Flutter has gained tremendous popularity as a cross-platform UI framework—and for good reason.
With a single codebase, you can target web, desktop, and mobile platforms while delivering a consistent, polished user experience. The declarative nature of Dart makes building comprehensive UIs intuitive and productive.
But what happens when you need to venture beyond Flutter’s comfort zone into low-level platform features?
Flutter’s Sweet Spot
From my development experience, Flutter truly excels at building applications that prioritize:
- Predictable user experience - Widgets behave consistently across platforms
- UI consistency - Your app looks and feels the same on iOS and Android
- Rapid development - Hot reload and declarative syntax speed up the iteration cycle
For most business applications, e-commerce apps, or content-driven platforms, Flutter is an excellent choice.
The framework abstracts away platform differences beautifully, letting developers focus on building features rather than handling platform quirks.
The Low-Level Challenge
However, the abstraction that makes Flutter powerful also becomes its limitation when you need granular control over platform-specific hardware features.
Our team encountered this firsthand when building a mobile application requiring direct access to the camera’s raw framebuffer.
The Problem with Existing Solutions
Even though plugins like camerawesome exist, they didn’t meet our requirements.
Our product needed a specialized decoding process that required direct framebuffer access.
We considered several approaches:
Option 1: Using dart:ffi
Dart provides dart:ffi for interfacing with C libraries through the C ABI.
In theory, this allows passing framebuffer pointers from native code to Dart. However, this approach has a critical flaw:
Native (C) → Platform Channel → Dart → dart:ffi → Native (C)
This is double work.
The framebuffer pointer originates from native C code, travels through Flutter’s platform channel to Dart, only to be passed back to native code via FFI.
This overhead is unnecessary and inefficient for performance-critical frame processing.
Option 2: Direct Native Integration
A better approach would be processing the framebuffer entirely at the native level without going through the platform channel:
Camera → Native Code (C/Rust) → Decoding → Flutter UI
This eliminates the round-trip through Dart but requires building a custom plugin from scratch.
Our Solution: Building a Custom Plugin with Rust
We decided to create our own plugin.
This decision was driven by two key requirements:
- Direct framebuffer access - Process frames at the native level
- Customizable scanning behavior - Fine-tune the decoding process for our specific use case
The Architecture
Our solution uses Rust as the core processing layer:
┌─────────────────────────────────────────────────────────────┐
│ Flutter UI (Dart) │
│ Platform Channel │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Native Plugin (Kotlin/Swift) │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Android (Kotlin) │ │ iOS (Swift/Obj-C) │ │
│ │ JNI → Rust lib │ │ Static Link → Rust lib │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Rust Library (Shared/Static) │
│ Frame Processing + Custom Decoding │
└─────────────────────────────────────────────────────────────┘
Why Rust?
We chose Rust for several compelling reasons:
1. Cross-platform compilation
Rust can compile to native libraries for both Android (as .so/.a) and iOS (static library).
2. C ABI compatibility
Rust exposes a C-compatible interface, allowing seamless integration with:
- Android: Loaded via JNI from Java/Kotlin
- iOS: Statically linked with Objective-C/Swift
3. Performance
Frame processing demands speed, and Rust delivers near-C performance.
4. Memory safety
Long-lived processing is prone to memory leaks.
Rust’s ownership model prevents this at compile time, making it ideal for continuous frame processing.
The Challenges
This approach wasn’t without hurdles:
Platform-Specific Implementation
We had to handle camera preview surface binding separately for each platform:
- Android: Java/Kotlin for camera management and surface binding
- iOS: Objective-C/Swift for AVCaptureSession configuration
This meant maintaining two separate native implementations that both interface with the same Rust core.
Complexity Overhead
The complete solution looks like this:
Rust → C library → Java/Kotlin (Android) / Obj-C/Swift (iOS) → Dart via Platform Channel
It’s undeniably complex.
For a pure Android app, you’d simply use the library directly. For a pure iOS app, you’d link it natively. Flutter’s cross-platform nature adds these additional layers.
The Verdict: Flutter’s Trade-offs
This experience highlights an important truth about Flutter:
Flutter does exactly what it’s designed for, and it does it very well.
When your application fits Flutter’s model—UI-driven, business logic in Dart, standard platform integrations—it shines.
But when you need deep platform integration or hardware-level control, you face a choice:
When to Use Flutter
- Standard UI-heavy applications
- Apps requiring consistent cross-platform behavior
- Projects with tight timelines for multiple platforms
- Business logic that can live entirely in Dart
When to Consider Alternatives
- Apps requiring extensive low-level hardware access
- Performance-critical real-time processing
- Deep platform-specific integrations
- Cases where native development for each platform is feasible
Conclusion
Our journey with Flutter and Rust taught us valuable lessons about choosing the right tool for the job.
Flutter remains an excellent framework for its intended use cases, but it’s crucial to evaluate the trade-offs before committing to it for projects requiring low-level platform access.
The “write once, run everywhere” promise comes with caveats. When you hit Flutter’s boundaries, be prepared to dive deep into native development—sometimes for multiple platforms simultaneously.
For teams considering Flutter for similar use cases: weigh the development velocity against the complexity of custom native integrations.
In our case, the trade-off was worth it because the rest of our application benefited from Flutter’s cross-platform nature.
But your mileage may vary.
Have you encountered similar challenges with Flutter? I’d love to hear about your experiences.