← Back to blog

Flutter: When Cross-Platform Convenience Meets Low-Level Reality

FlutterRustMobile DevelopmentFFICameraCross-Platform

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:

  1. Direct framebuffer access - Process frames at the native level
  2. 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.