Zero-Boilerplate Rust Integration: Using Flutter Native Assets and flutter_rust_bridge
Bringing Rust into Flutter used to mean writing a plugin, hand-editing Gradle/Xcode projects, and maintaining multiple platform scripts. With Flutter Native Assets and flutter_rust_bridge (FRB) v2, you can ship high-performance Rust code in a Flutter app with almost zero platform boilerplate. This guide walks through a production-ready setup that pairs Flutter Native Assets with FRB to deliver a smooth “just works” developer experience for Dart FFI Rust integration.
What you’ll build:
- A minimal Rust crate exposing functions to Flutter via flutter_rust_bridge
- Native Assets wiring so the Rust dynamic library is bundled automatically
- A clean Flutter-side API with Riverpod for DI and testing
Primary use case: high performance Flutter native code without writing platform plugins, while keeping the developer ergonomics of idiomatic Dart.
Prerequisites
- Flutter SDK: 3.22+ (Dart 3.4+ recommended for stable Native Assets).
- Rust toolchain: stable 1.75+ with rustup.
- Android: Android Studio with NDK r26+, rustup Android targets installed.
- iOS/macOS: Xcode 15+, rustup Apple targets installed.
- Windows/Linux: recent Visual Studio/Clang toolchains per platform.
You’ll also install these packages in your Flutter app:
- flutter_rust_bridge: ^2.x
- ffi: ^2.x
- flutter_rust_bridge_codegen: ^2.x (dev dependency)
Note: FRB v2 integrates with Flutter Native Assets so you don’t have to write platform plugins or hand-wire Gradle/Podspec logic.
Why Flutter Native Assets for Rust?
Native Assets is a Dart/Flutter feature that:
- Builds and bundles native dynamic libraries per target platform/ABI.
- Exposes those libraries at runtime to Dart FFI loaders automatically.
- Eliminates most platform boilerplate you’d otherwise maintain.
flutter_rust_bridge v2 layers on top:
- Generates idiomatic Dart bindings and Rust glue.
- Spawns background threads (by default) so CPU-heavy calls don’t block the UI thread.
- Handles marshalling for common types (strings, lists, structs, enums, results).
- Plays nicely with hot reload and the Flutter toolchain.
Result: a clean dart:ffi Rust integration with almost no platform-specific code.
Project structure
Recommended layout inside your Flutter app:
- lib/
- rust/
- bridge_generated.dart (generated)
- bridge_definitions.dart (generated, optional)
- rust_api.dart (handwritten facade + DI)
- rust/
- rust/
- Cargo.toml
- src/
- api.rs
- bridge_generated.rs (generated)
This keeps Rust next to your app, avoids a separate plugin, and works across Android, iOS, macOS, Windows, and Linux.
Step 1: Add dependencies
pubspec.yaml:
name: frb_native_assets_demo
description: Flutter + Rust with Native Assets and flutter_rust_bridge
environment:
sdk: ">=3.4.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
ffi: ^2.1.0
flutter_rust_bridge: ^2.0.0
flutter_riverpod: ^2.5.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_rust_bridge_codegen: ^2.0.0
build_runner: ^2.4.9
Notes:
- flutter_rust_bridge (runtime) is a normal dependency.
- flutter_rust_bridge_codegen is a dev dependency used to generate bindings.
- ffi is required for Dart FFI.
Step 2: Create the Rust crate
Inside your Flutter project root:
mkdir -p rust/src
cd rust
cargo init --lib
Edit rust/Cargo.toml:
[package]
name = "frb_native_assets_demo"
version = "0.1.0"
edition = "2021"
[lib]
# cdylib produces a C-compatible dynamic library for FFI
crate-type = ["cdylib"]
[dependencies]
# FRB v2 runtime + macros
flutter_rust_bridge = "2"
flutter_rust_bridge_macros = "2"
# Example: fast hashing
sha2 = "0.10"
hex = "0.4"
# For async-friendly defaults (optional; FRB can spawn threads)
once_cell = "1"
Create rust/src/api.rs:
// rust/src/api.rs
use flutter_rust_bridge::frb; // procedural macro
use sha2::{Digest, Sha256};
#[frb] // Expose to Dart
pub fn greet(name: String) -> String {
format!("Hello, {name} from Rust!")
}
#[frb] // Expose as an async-friendly task by default (returns Future<String> in Dart)
pub fn sha256_hex(input: Vec<u8>) -> String {
let mut hasher = Sha256::new();
hasher.update(&input);
let out = hasher.finalize();
hex::encode(out)
}
Do not edit bridge_generated.rs; it will be generated.
Step 3: Generate bindings
From the Flutter project root:
dart run flutter_rust_bridge_codegen \
--rust-input rust/src/api.rs \
--dart-output lib/rust/bridge_generated.dart \
--rust-output rust/src/bridge_generated.rs \
--dart-decl-output lib/rust/bridge_definitions.dart
What this does:
- Generates
bridge_generated.dartandbridge_definitions.dartin your Flutter lib/ for a nice Dart API. - Generates
bridge_generated.rsto glue Rust functions into FRB’s runtime.
Re-run this command whenever you change public Rust APIs.
Tip: add a simple script or Makefile target to regenerate bindings.
Step 4: Wire up Flutter Native Assets
With Native Assets, you don’t write platform-specific build files. The flutter_rust_bridge toolchain builds your Rust cdylib per target and registers it so Flutter can bundle and dlopen it at runtime.
Minimal setup:
- Keep your Rust crate at rust/ with crate-type cdylib (already done).
- Depend on flutter_rust_bridge (Dart) as shown.
- Use the FRB-generated loader in Dart (shown next).
- Ensure platform targets are installed (see Platform targets section).
When you run flutter run or flutter build, the Native Assets flow compiles the Rust library for the active platform, places it in the right location, and makes it discoverable for dart:ffi without editing Gradle/CMake/Podspec.
If you use CI or custom build invocations, you can prebuild the Rust artifacts with cargo for each target; Flutter will still bundle them via Native Assets.
Step 5: A clean Dart API (with Riverpod)
lib/rust_api.dart:
import 'dart:typed_data';
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' as frb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'rust/bridge_generated.dart'; // generated
// FRB generates a class (naming may vary) with a Native Assets aware loader.
// In recent FRB v2, `createLib()` / `RustLibImpl` style APIs are provided.
// The generated file exports a top-level `createLib()` that returns your API facade.
late final RustApi _rust; // Replace `RustApi` with the generated API class type if different.
final rustApiProvider = Provider<RustFacade>((ref) {
return RustFacade(_rust);
});
class RustFacade {
RustFacade(this._api);
final RustApi _api; // Replace with actual generated type
Future<String> greet(String name) => _api.greet(name: name);
Future<String> sha256Hex(Uint8List data) => _api.sha256Hex(input: data);
}
// Initialize on app startup
Future<void> initRust() async {
// The generated `createLib()` knows how to load the Native Asset (no manual dlopen).
_rust = await createLib(); // or `RustLibImpl.create()` depending on generated API
}
Notes:
- FRB v2’s generated Dart code provides a Native Assets-aware loader, so you don’t need to call
DynamicLibrary.openor ship per-platform paths. - FRB defaults to offloading functions to a background worker (when not marked sync), returning Dart Futures and keeping the UI smooth.
Example usage in your app:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'rust_api.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initRust();
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final rust = ref.watch(rustApiProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter Native Assets + Rust')),
body: Padding(
padding: const EdgeInsets.all(16),
child: FutureBuilder(
future: rust.greet('Flutter'),
builder: (context, snap) {
final msg = snap.data ?? '…';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(msg),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () async {
final out = await rust.sha256Hex(
Uint8List.fromList('hello world'.codeUnits),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('sha256: $out')),
);
}
},
child: const Text('Compute sha256 in Rust'),
),
],
);
},
),
),
),
);
}
}
Step 6: Install platform targets (once)
The Native Assets toolchain will build appropriate Rust artifacts per platform. Ensure targets are installed:
Android:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
# Ensure ANDROID_NDK_ROOT or ANDROID_NDK_HOME is set (NDK r26+).
iOS/macOS:
# iOS device + simulator (Apple Silicon simulator is aarch64)
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
# macOS (x64 and Apple Silicon)
rustup target add x86_64-apple-darwin aarch64-apple-darwin
Windows:
# MSVC toolchains
rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc
Linux:
# Most distros use gnu
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
You can now run:
flutter run
The Rust cdylib will be built and bundled via Flutter Native Assets automatically.
Testing strategies
- Unit tests (Dart): Structure your Dart facade behind an interface (e.g., RustFacade) and provide a fake for pure Dart tests.
- Integration tests: Run on each target platform/device/emulator to validate FFI loading and behavior. FFI requires a host OS, so your integration tests should run on-device (for Android/iOS) or host runners (for desktop).
- Rust tests: Add
#[cfg(test)]unit tests on the Rust side for algorithmic correctness (fast and deterministic).
Example fake for DI:
class FakeRustFacade extends RustFacade {
FakeRustFacade() : super(_FakeApi());
}
class _FakeApi implements RustApi {
@override
Future<String> greet({required String name}) async => 'Hello, $name (fake)';
@override
Future<String> sha256Hex({required Uint8List input}) async => 'deadbeef';
}
Performance tips for high performance Flutter native code
- Batch work across the FFI boundary. Many small calls are slower than one larger call.
- Prefer
Uint8Listfor binary data; it maps efficiently toVec<u8>in Rust via FRB. - Keep long-running Rust work off the UI thread. FRB functions are async by default; avoid
syncunless the function is trivial. - Profile with Flutter DevTools and Rust profilers (e.g., perf, Instruments, Xcode, Windows Performance Analyzer) to find hot paths.
- Consider
#[inline]and release builds for Rust (--release) when benchmarking. Flutter release builds will also strip and optimize native code. - Minimize allocations on both sides; reuse buffers where practical.
Common pitfalls and troubleshooting
-
Android NDK not found
- Symptom: cargo/cc failing with messages about missing toolchains.
- Fix: Install NDK r26+, set ANDROID_NDK_ROOT or ANDROID_NDK_HOME, ensure Android SDK/NDK installed via Android Studio.
-
iOS simulator architecture mismatch
- Symptom: “no suitable image found” or “bad CPU type” for simulator.
- Fix: Ensure you have aarch64-apple-ios-sim target installed (Apple Silicon). For Intel simulators, x86_64-apple-ios is required. The Native Assets build should produce the right slice; make sure you run against the matching simulator.
-
Codesigning on iOS/macOS
- Symptom: App won’t launch due to signing or entitlement issues.
- Fix: Build/run from Xcode/Flutter with a valid team. Native Assets embeds the library within the app bundle; Xcode signs it as part of the app signing step.
-
Desktop linker not found
- Symptom: “linker not found” on Windows or Linux.
- Fix: Install Visual Studio C++ components on Windows (MSVC), and system toolchains/Clang on Linux. Verify rustup default host triple matches your toolchain.
-
Hot reload vs native code
- Symptom: Changes to Rust not reflected after hot reload.
- Fix: Rust changes require a rebuild of the dynamic library. Stop and restart flutter run to trigger a Native Assets rebuild.
-
Large payload copies
- Symptom: Unexpected overhead passing large arrays across FFI.
- Fix: Batch data, compress when appropriate, or process in-place in Rust. FRB handles efficient marshaling for
Uint8List/Vec<u8>, but avoid round-tripping megabytes repeatedly.
Clean architecture and DI in production
- Keep FRB-generated code in lib/rust/.
- Wrap generated APIs behind a domain interface (e.g.,
CryptoRepository) and inject with Riverpod/Provider/BLoC. - Expose high-level use cases (e.g.,
computeDocumentDigest) rather than leaking low-level FFI calls across your app. - Add a fake/mock implementation for pure Dart unit tests, and an integration test target for end-to-end verification.
Example repository facade:
abstract class CryptoRepository {
Future<String> sha256Hex(Uint8List data);
}
class RustCryptoRepository implements CryptoRepository {
RustCryptoRepository(this._rust);
final RustFacade _rust;
@override
Future<String> sha256Hex(Uint8List data) => _rust.sha256Hex(data);
}
final cryptoRepoProvider = Provider<CryptoRepository>((ref) {
final rust = ref.watch(rustApiProvider);
return RustCryptoRepository(rust);
});
Security and release builds
- Ship release-mode Rust libraries with
--releasefor size and speed. - Consider stripping symbols for production; debug symbols can be archived separately.
- If you handle untrusted input, audit Rust crates and consider sandboxing strategies where relevant.
- Validate that your app store packaging includes the correct ABIs (Android) and architectures (iOS/macOS).
A quick checklist for teams
- FRB codegen runs from CI (fail on drift)
- Rust targets installed on CI workers
- Integration tests run on at least one device/simulator per platform
- Release builds use Rust
--release - Crash reporting collects native stack traces (configure symbolication)
- Repository interface abstracts FFI for testing and maintainability
Common Questions & Edge Cases
-
Do I still need a Flutter plugin?
- No. With Flutter Native Assets, you don’t need a platform plugin for typical
dart:ffiRust integration.
- No. With Flutter Native Assets, you don’t need a platform plugin for typical
-
How do I call synchronous Rust code?
- FRB provides
#[frb(sync)]for functions that can be called synchronously. Prefer async defaults for CPU-heavy work to avoid blocking Dart’s main isolate.
- FRB provides
-
Is this approach supported on all platforms?
- Yes, Flutter Native Assets and FRB support Android, iOS, macOS, Windows, and Linux. Ensure the correct Rust targets/toolchains are installed.
-
Can I share Rust across multiple Flutter packages?
- Yes. You can keep the Rust crate in a separate package or monorepo module, as long as it’s reachable by the Native Assets builder during
flutter run/build.
- Yes. You can keep the Rust crate in a separate package or monorepo module, as long as it’s reachable by the Native Assets builder during
Conclusion
With flutter_rust_bridge native assets, adding Rust to Flutter is no longer a “plugin project.” You write Rust functions, run FRB codegen, and let Flutter Native Assets handle building and bundling for each platform. The result is a clean, testable Dart API backed by Rust’s performance - ideal for cryptography, compression, search, data processing, and any CPU-heavy logic you want off the UI thread.
Key takeaways:
- Use flutter native assets rust to eliminate platform boilerplate.
- Let flutter_rust_bridge generate safe, ergonomic bindings with async defaults.
- Keep FFI behind interfaces (Riverpod, Provider, BLoC) to preserve a clean architecture.
- Batch work across the FFI boundary and profile both sides for the best results.
Next steps:
- Add more Rust APIs (streams, structs, enums) and regenerate bindings.
- Automate codegen and Rust builds in CI.
- Explore incremental optimization (SIMD, zero-copy patterns) for your domain.
Happy shipping high-performance Flutter native code - without the boilerplate.
