When you’re building a Flutter application that needs to talk to hardware, there’s a question that surfaces quickly: where does the systems-level code actually live?
It’s an architecture question—and the answer determines whether your team spends cycles chasing memory bugs in production or building features.
At VGV, we saw this challenge play out firsthand while teaming up with Toyota on their in-vehicle infotainment system (IVI). In that project, Rust handles communication with the vehicle’s computer: real-time data feeds, sensor inputs, and low-level control signals. Flutter owns state management and UI rendering. The result is a clean architectural boundary that lets each layer do what it does best. Let’s look at why that division of responsibility makes sense, and how you can apply the same pattern in your own projects.
The Problem with Reaching Too Far
Flutter is an exceptional framework for building expressive, performant user interfaces across platforms. But it was designed to render UI—not to manage shared memory, handle unsafe pointer arithmetic, or guarantee the absence of data races in a concurrent systems environment.
When embedded or native system code is involved, the typical approach is to reach for platform channels: write the systems-level logic in C, C++, then call it from Dart through FFI or a plugin interface. That works, but it comes with a cost. C and C++ are powerful precisely because they give you direct control over memory, but that power comes with responsibility—they’ll let you misuse that control in ways that produce subtle, hard-to-reproduce bugs that only surface in production, often under load. Consider referencing Flutter plugin generation with Very Good CLI for an alternative approach to native integration.
For a consumer infotainment system inside a vehicle, “subtle bug that surfaces under load” is not an acceptable failure mode.

How Rust Fills This Gap
Rust was built to solve the exact problem that makes systems programming risky: it gives you low-level control without sacrificing memory safety. Its ownership and borrowing model enforces at compile time the discipline that C and C++ demand you maintain manually.
Here’s what that means in practice for a Flutter data layer:
Memory safety without a garbage collector. Rust’s ownership model eliminates entire classes of bugs (null pointer dereferences, use-after-free, data races) at compile time. You get native performance without GC unpredictability or the memory-safety risks of C/C++. For a Flutter app, this means the data layer is fast and correct by construction, not by careful discipline. For a deeper understanding, see the Rust’s ownership model in the official Rust Book.
Fearless concurrency. Vehicle computers push data constantly (speed, throttle position, battery state, sensor readings) across multiple threads. Rust’s type system makes it impossible to share mutable state across threads without explicit synchronization. You can’t accidentally introduce a data race. This means the development team can reason about concurrent behavior with certainty—no threads silently corrupting shared state in production. That certainty compounds as the codebase grows. For more details, explore Rust’s fearless concurrency model.
A strong FFI story. Rust can expose a C-compatible interface, which means it integrates naturally with Dart’s FFI documentation. You get the expressiveness and safety of Rust at the systems level, and the ergonomics of Dart and Flutter at the UI level, without an awkward glue layer in between.
Cross-platform compilation. A Rust library compiles to native code on every major platform Flutter targets: iOS, Android, macOS, Linux, and Windows. Write the data layer once; deploy it everywhere. For teams shipping to multiple platforms, this reduces both code surface area and maintenance burden. Learn more about Flutter’s cross-platform capabilities.
Explicit error handling. Rust’s Result type forces you to handle errors at the call site. There are no unchecked exceptions that silently propagate and manifest as crashes at a layer far removed from the actual failure. When you’re interfacing with hardware that can fail in a dozen ways, explicit error paths aren’t a burden—they’re a feature.

The Architecture in Practice
The Toyota IVI project illustrates this pattern clearly. The system has three conceptual layers:
- The vehicle computer: proprietary hardware pushing real-time signals.
- The Rust data layer: responsible for consuming those signals, transforming them into structured data, and exposing a clean API to the layer above.
- The Flutter layer: responsible for state management (consuming the Rust API) and rendering the UI.
Flutter never knows the vehicle computer exists. It only knows about the structured data types and stream interfaces the Rust layer exposes. This separation has two important consequences.
First, the Flutter team and the vehicle systems team can work independently. The interface contract between Rust and Flutter is defined in code; as long as both sides honor it, they don’t need to be in the same room—or even on the same continent. This architectural separation enables the Very Good Layered Architecture approach, where data, domain logic, and presentation layers are cleanly separated.
Second, the Rust layer is independently testable. Business logic that lives in Rust can be unit tested in Rust, without a Flutter harness or an emulated device. This tightens the feedback loop for the development team and reduces the risk of integration-time regressions. See independently testable systems for VGV’s testing philosophy.

The Packages That Make This Work
Two libraries connect Rust and Flutter effectively. They take different approaches, and the right choice depends on your use case.
membrane
Toyota utilizes the membrane package in their IVI project. It generates Dart bindings from annotated Rust code, with first-class support for async streams—which is exactly what you need when consuming continuous data from a hardware source. The ergonomics are tight: you annotate your Rust functions and types, run the generator, and get idiomatic Dart on the other side. The generated code feels native to Flutter, not like a foreign interface bolted on.
membrane is purpose-built for this pattern, which means it’s opinionated in useful ways. Membrane was made specifically for IVI’s architecture pattern: continuous data flowing from Rust into Flutter. Starting with the tool that matches your use case saves integration headaches later. If your use case involves real-time data flowing from a Rust backend into a Flutter UI, it’s worth starting here.
flutter_rust_bridge
flutter_rust_bridge is the broader, community-standard solution for adding Rust to Flutter projects. It supports a wide range of types, handles async operations, and integrates well with the Flutter build system across all major platforms. The documentation is thorough, the community is active, and the project is well-maintained.
If you’re starting a new project and want a battle-tested, general-purpose integration layer, flutter_rust_bridge is the right starting point. It lacks membrane’s stream-first ergonomics but covers a wider range of use cases and provides more examples.
When to Reach for This Pattern
Rust as a Flutter data layer isn’t the right answer for every application. If your data layer is a REST API and a local SQLite database, this is overkill.
Rust as a Flutter data layer makes sense primarily when correctness is non-negotiable (crashes or data corruption have real-world consequences). The benefits compound if any of these are also true:
- You’re interfacing with hardware, sensors, or proprietary protocols.
- You’re targeting multiple platforms and want a single, unified systems implementation.
- Your team already has Rust expertise, or the systems team is separate from the Flutter team.
- You need predictable performance characteristics without GC interference.
The Toyota IVI project sits at the intersection of all five. Your project probably won’t. But if it hits even two or three of them, the architecture earns its complexity. Learn more about VGV’s scalable architecture best practices.
Where to Go From Here
Architectural clarity compounds over time. Define the boundary between your systems and UI layers explicitly, rather than letting it blur as the codebase grows. The key insight is that this decision framework applies broadly—whether you’re using Rust, C++, or another systems language, the architectural boundaries matter more than the specific tool.
If you’re building something that lives close to hardware, we’d encourage you to prototype the pattern before committing. Stand up a minimal Rust library, expose a few functions via flutter_rust_bridge or membrane, and integrate them into a Flutter app. The build tooling has matured considerably, and the path from zero to working is now short.
At VGV, this is the kind of architecture problem we find genuinely interesting—the intersection of systems constraints and product experience, where the right technical decision has a direct effect on what a user sees and feels. If you’re working on something in that space and want to think it through, we’d love to hear about it. Explore VGV’s work on enterprise Flutter projects to see how we’ve applied architectural clarity in production systems.