Benefits of Using Rust in Flutter Apps
When integrating Rust into Flutter, the main idea is to let Flutter handle the UI/UX while Rust powers the core logic. This combination provides several benefits:
🚀 High Performance
Rust compiles directly to native machine code, making it ideal for performance-critical tasks such as cryptography, media processing, networking, or financial calculations.
📱 Cross-Platform Core Logic
With Rust, you write your business logic once and use it across both Android and iOS. This avoids duplicating complex code in Kotlin and Swift, and ensures consistent behavior across platforms.
🛡️ Memory Safety
Rust’s strong type system and ownership model catch many bugs at compile time, reducing crashes and improving app reliability. This makes Rust a safe choice for handling sensitive logic like authentication or encryption.
⚡ Concurrency and Async
Rust provides powerful tools for multithreading and asynchronous programming, which allows you to run heavy background tasks without blocking Flutter’s UI. This is essential for apps with real-time data, chat, or streaming features.
📦 Rich Ecosystem
The Rust ecosystem offers a wide range of crates (libraries) for cryptography, networking, databases, serialization, and even machine learning. By reusing these crates, you can quickly add advanced capabilities to your app.
🔧 Maintainability
By keeping your core logic in one shared Rust library, you reduce code duplication and simplify long-term maintenance. Updates are easier to roll out since both iOS and Android share the same core implementation.
✅ In summary: Flutter + Rust lets you build apps where Flutter delivers the smooth user interface, and Rust delivers the speed, safety, and cross-platform consistency for the underlying logic. This architecture scales well from small prototypes to large production apps.
Real-World Applications Using Embedded Native Code
Many production applications embed native code (C++, Rust, or other compiled languages) inside their mobile apps to handle performance-critical, cross-platform logic. This is exactly the approach we are exploring with Rust + Flutter. Here are some notable examples:
🔹 Telegram
- What they do: Telegram uses a C++ core for their messaging engine, encryption, and network protocol handling.
- Why they do it: By embedding C++ inside Android and iOS apps, Telegram ensures high performance, reliability, and consistent behavior across platforms.
- Takeaway: This shows that even large-scale apps rely on a compiled, cross-platform core for heavy business logic while keeping the mobile UI in native frameworks (or Flutter-like layers).
🔹 Signal
- What they do: Signal embeds native libraries (C/C++) for encryption and secure messaging.
- Why they do it: Critical cryptographic operations need to be fast and memory-safe, which is difficult to achieve with pure Java/Kotlin or Swift alone.
🔹 Discord
- What they do: Discord uses C++ for parts of their audio/video pipeline in mobile apps.
- Why they do it: Real-time media processing requires low latency and high efficiency, which is best handled in a compiled language.
🔹 Brave Browser (Mobile)
- What they do: Brave embeds a native Chromium engine (C++) in mobile apps.
- Why they do it: To ensure consistent rendering, speed, and security, the same native engine runs across iOS and Android.
Summary: Embedding compiled, cross-platform code inside mobile apps is a proven production approach. Companies like Telegram, Signal, Discord, and Brave rely on it to deliver performance-critical features while keeping mobile UIs responsive. Using Rust in Flutter is a modern, safe alternative to this same strategy, giving developers high performance, memory safety, and cross-platform code reuse.
TL;DR
- We'll build a small Rust library that exposes a C ABI (
extern "C"
) and load it from Flutter using dart:ffi. - For Android: build
.so
for each ABI, place them underandroid/app/src/main/jniLibs/<ABI>/libmylib.so
and Flutter will pick them up. - For iOS: produce an XCFramework (or link a static
.a
) and add it to the Xcode Runner app; then useDynamicLibrary.process()
or open the framework. - Tools that make life much easier (optional but recommended):
cargo-ndk
(Android),cargo-lipo
(iOS), andcbindgen
(generate headers).
This doc contains reproducible commands, Rust and Dart snippets, integration steps and a short debugging checklist.
Assumptions / prerequisites
Make sure you have the following installed and configured on your machine:
- Flutter and a working Flutter project. (
flutter --version
) - Rust toolchain (rustup + cargo). (
rustc --version
,cargo --version
) - Android SDK + Android NDK (for Android builds). You can install via Android Studio or sdkmanager.
- Xcode (for iOS builds) and CocoaPods (for Flutter iOS plugins).
- Optional helpers (recommended):
-
cargo-ndk
(cargo install cargo-ndk
) – cross-compile for Android easily. -
cargo-xcode
(cargo install cargo-xcode
) – build iOS universal/static libs. For iOS, prefer cargo-xcode overcargo-lipo
-
cbindgen
(cargo install cbindgen
) – generate C headers from Rust code.
-
Tip: Keep
rustup
updated and add targets for the platforms you will build for.
High-level approaches — pick one
Manual dart:ffi approach (this tutorial focuses on this) — you write
extern "C"
APIs in Rust, build platform libraries, and call them from Dart viadart:ffi
. This is generic and minimal-dependency; great for an educational article.Use a codegen bridge such as
flutter_rust_bridge
oruniffi
— they generate ergonomic bindings and often handle async/callbacks/serialization for you. Recommended for production complexity, but they require extra setup and will need codegen steps in your workflow. (I can prepare a separate tutorial forflutter_rust_bridge
if you want.)
Approach 1 — Manual Integration (Full Control)
This approach gives you maximum flexibility. You manually create a Rust crate inside your Flutter project, configure Cargo.toml, build libraries for Android with cargo-ndk and for iOS with cargo-xcode or xcodebuild, and generate bindings with flutter_rust_bridge_codegen. You then load the generated bindings in Dart using DynamicLibrary.
This method is ideal if you want to deeply understand the build process, fine-tune platform integration, or add Rust into an existing Flutter app without scaffolding a new one.
Project layout suggested
my_flutter_app/
├─ android/
├─ ios/
├─ lib/
├─ rust_native/ # new Rust crate lives here
│ ├─ Cargo.toml
│ └─ src/lib.rs
└─ README.md
Put all Rust code in rust_native/
so builds are contained.
1. Create the Rust library crate
From your project root:
cd my_flutter_app
cargo new --lib rust_native
cd rust_native
Edit Cargo.toml
to export C-compatible library artifacts:
[package]
name = "mylib"
version = "0.1.0"
edition = "2021"
[lib]
name = "mylib"
crate-type = ["cdylib", "staticlib"]
[dependencies]
# add dependencies here if needed
crate-type = ["cdylib", "staticlib"]
tells Cargo to build both shared libraries and static libs depending on the target.
Create src/lib.rs
with a tiny example API that uses a C ABI:
// src/lib.rs
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
// Example returning a string (caller must free the returned pointer):
use std::ffi::CString;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn rust_hello() -> *mut c_char {
let s = CString::new("Hello from Rust!\n").unwrap();
s.into_raw() // ownership transferred to caller
}
#[no_mangle]
pub extern "C" fn rust_string_free(s: *mut c_char) {
if s.is_null() { return; }
unsafe { CString::from_raw(s); } // drops and frees memory
}
Notes:
-
#[no_mangle]
keeps the symbol name stable. -
extern "C"
ensures C ABI compatibility (necessary fordart:ffi
). - For strings we hand out an allocated
char*
and also export afree
function so the Dart side can release memory.
2. (Optional) Generate a C header with cbindgen
cbindgen
can generate a .h
header from your Rust signatures. This is useful for iOS Xcode integration or for human documentation.
Install and run:
cargo install cbindgen
cbindgen --lang c --output include/mylib.h
This will produce include/mylib.h
you can add to Xcode or include with -I
flags.
3. Install Android NDK
In Android Studio:
- Open Tools → SDK Manager → SDK Tools
- Install NDK (Side by side) and CMake
Then set environment variables:
# Linux/macOS
export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/<version>
export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
# Windows (PowerShell)
setx ANDROID_NDK_HOME "C:\Users\<you>\AppData\Local\Android\Sdk\ndk\<version>"
Verify:
echo $ANDROID_NDK_HOME
ls $ANDROID_NDK_HOME
4. Build for Android (recommended: use cargo-ndk
)
Install cargo-ndk
if you like:
cargo install cargo-ndk
Then from the rust_native
directory, run:
# target ABIs you want: armeabi-v7a, arm64-v8a, x86, x86_64
cargo ndk -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 --build-type release build --release --lib
If successful, you'll find .so
files under target/<target>/release/
(or under a cargo-ndk output folder). The shared library name will be libmylib.so
.
Copy .so
to your Flutter Android project
Create (if missing) the jniLibs directory and ABI subfolders in your Flutter Android module:
android/app/src/main/jniLibs/arm64-v8a/
android/app/src/main/jniLibs/armeabi-v7a/
android/app/src/main/jniLibs/x86/
android/app/src/main/jniLibs/x86_64/
Copy each built .so
to the corresponding folder and rename them libmylib.so
(they likely already are named this way):
cp path/to/target/aarch64-linux-android/release/libmylib.so ../android/app/src/main/jniLibs/arm64-v8a/
# repeat for other ABIs...
Gradle will automatically package those .so
files into the APK/AAB.
If you do not want to commit binary artifacts into git, you can script the build/copy step in a shell script (good idea for CI).
5. Build for iOS (create an XCFramework or link static lib)
Option A — recommended: produce an XCFramework using cargo xcode
and xcodebuild
From rust_native
run:
# cargo xcode builds universal static libs for iOS targets
cargo xcode --release
Now create an XCFramework (so it works for device + simulator):
# create headers directory (use the cbindgen-generated header or write a small header)
mkdir -p include
cbindgen --output include/mylib.h
# create the xcframework bundling device + simulator libs (if you have separate .a files for each, pass them)
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libmylib.a -headers include \
-library target/x86_64-apple-ios/release/libmylib.a -headers include \
-output MyRustLib.xcframework
On modern Apple Silicon macs you may need to build for
aarch64-apple-ios-sim
as well. Ifcargo-lipo
does not produce all the slices you need, build the targets individually (--target
) and pass both.a
files toxcodebuild
.
Option B — simpler (static .a
) and link it in Xcode
If you prefer a static lib only for device (or for a single arch), you can build directly:
rustup target add aarch64-apple-ios x86_64-apple-ios
cargo build --target aarch64-apple-ios --release
cargo build --target x86_64-apple-ios --release
Then add the two libmylib.a
to xcodebuild -create-xcframework ...
as above.
Add the XCFramework to your Runner
- Open
ios/Runner.xcworkspace
in Xcode. - Drag
MyRustLib.xcframework
into the Runner project (select the Runner target andCopy items if needed
). - In the target Build Phases → Link Binary With Libraries, ensure the XCFramework is linked.
- If you used a C header, set Header Search Paths (or add the header to the project) so the project knows about the header for compile-time only.
When there is a static library linked into the app binary, from Dart you can access the symbols using DynamicLibrary.process()
; if you embed a dynamic framework you can DynamicLibrary.open("MyRustLib.framework/MyRustLib")
.
6. Dart side — calling Rust with dart:ffi
Add a small Dart wrapper around dart:ffi
.
lib/rust_bindings.dart
:
import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
// Native typedefs
typedef rust_hello_native = ffi.Pointer<Utf8> Function();
typedef rust_string_free_native = ffi.Void Function(ffi.Pointer<Utf8>);
typedef rust_add_native = ffi.Int32 Function(ffi.Int32, ffi.Int32);
// Dart typedefs
typedef RustHello = ffi.Pointer<Utf8> Function();
typedef RustStringFree = void Function(ffi.Pointer<Utf8>);
typedef RustAdd = int Function(int, int);
// Binding class
class RustBindings {
late ffi.DynamicLibrary _dylib;
late RustAdd rustAdd;
late RustHello hello;
late RustStringFree free;
RustBindings() {
if (Platform.isAndroid) {
_dylib = ffi.DynamicLibrary.open('libmylib.so');
} else if (Platform.isIOS) {
// If library is linked statically into the app:
_dylib = ffi.DynamicLibrary.process();
// If you embedded a dynamic framework, use:
// _dylib = ffi.DynamicLibrary.open('MyRustLib.framework/MyRustLib');
} else {
_dylib = ffi.DynamicLibrary.open('libmylib.dylib'); // macOS
}
rustAdd = _dylib
.lookup<ffi.NativeFunction<rust_add_native>>('rust_add')
.asFunction();
hello = _dylib
.lookupFunction<rust_hello_native, RustHello>('rust_hello');
free = _dylib
.lookupFunction<rust_string_free_native, RustStringFree>('rust_string_free');
}
}
7. Use in Flutter App
import 'package:flutter/material.dart';
import 'rust_bindings.dart';
void main() {
final rust = RustBindings();
runApp(MyApp(rust: rust));
}
class MyApp extends StatelessWidget {
final RustBindings rust;
const MyApp({Key? key, required this.rust}) : super(key: key);
@override
Widget build(BuildContext context) {
int result = rust.rustAdd(2, 3);
print('2 + 3 = $result');
final rustMessage = rust.hello();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Rust + Flutter FFI")),
body: Center(
child: Text(rustMessage, style: TextStyle(fontSize: 24)),
),
),
);
}
}
Important: Always provide a free function on the Rust side if Rust allocated the memory (to avoid mismatched allocators).
8. Flutter build & run
Android
-
flutter run
orflutter build apk
should package the.so
files fromandroid/app/src/main/jniLibs/*
automatically. - If your binary isn't found at runtime check
adb logcat
forUnsatisfiedLinkError
and double-check the path & ABI.
iOS
-
flutter build ios
then openios/Runner.xcworkspace
and run from Xcode (select a device or simulator). Pod install will run when needed. - If the app crashes or the symbol isn't found, check Xcode's Linker errors and ensure the XCFramework or
.a
is added & linked.
9. Common pitfalls & troubleshooting
-
Symbol not found at runtime: name mismatch (
#[no_mangle]
) or wrong function signature. Usenm
(mac/linux) orreadelf -s
to inspect exported symbols. -
Architecture mismatch: Ensure you built the library for the ABI the device expects. Use
file
orlipo -info
on built binaries. -
iOS simulator on Apple Silicon: you may need to produce
aarch64
slices for the simulator.cargo-lipo
+ buildingaarch64-apple-ios-sim
can help. -
Dart FFI type mismatch: Ensure C types map to the correct Dart FFI types (e.g.,
int32
↔Int32
). - Memory ownership: If Rust returns allocated memory, ensure you expose a free function and always call it from Dart.
10. Advanced: async, callbacks and threading
-
Dart
dart:ffi
cannot call back into Dart from arbitrary native threads. If you need callbacks, either:- Use platform channels to send messages (bridged by the Flutter engine), or
- Use
NativePort
/Dart_PostCObject
from the native side (advanced), or - Use
flutter_rust_bridge
oruniffi
which provide patterns for callbacks and async.
For long-running tasks, run native work on native threads and expose a future-like API to Dart (e.g. start job → poll result or use a callback mechanism).
This guide shows the simplest way to use Rust inside a Flutter app using the official flutter_rust_bridge
tooling. It uses the new generate
+ integrate
commands (replacing the old --watch
flag).
Conclusion:
The manual integration approach is best suited for developers who need fine-grained control over build artifacts, or those integrating Rust into an existing Flutter project. While it requires more setup (NDK paths, Rust targets, iOS frameworks), it ensures you fully understand how Flutter and Rust communicate, making debugging and customization easier in the long run.
Approach 2 — Quickstart with FRB create Command (Easiest)
This approach uses the FRB-provided scaffold tool to generate a new project where Flutter and Rust are already wired together.
1. Install prerequisites
Make sure you have installed:
- Flutter SDK
- Rust (
rustup
,cargo
) - Android Studio (for Android builds) / Xcode (for iOS builds)
Then install the FRB codegen tool (only once):
cargo install flutter_rust_bridge_codegen
2. Create a new Flutter + Rust project
Run the FRB helper to scaffold a full project:
flutter_rust_bridge_codegen create my_app
This generates a project with both Flutter and Rust already wired together:
my_app/
├─ lib/ # Flutter project
├─ rust/ # Rust crate
└─ rust_builder/ # Glue + generated bindings
3. Run the project
Move into the Flutter directory and run it like any Flutter app:
cd my_app/flutter
flutter run
You should see the sample Flutter UI calling a Rust function.
4. Modify Rust code
Open my_app/rust/src/api.rs
and edit or add Rust functions. For example:
pub fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
5. Regenerate bindings (new workflow)
Whenever you change your Rust API, run:
flutter_rust_bridge_codegen generate
flutter_rust_bridge_codegen integrate
-
generate
→ scansapi.rs
and produces new Dart + Rust glue code -
integrate
→ updates the Flutter project with the generated bindings
Then re-run Flutter:
flutter run
6. Dart side usage
FRB generates <project_name>_generated.dart
with ready-to-use bindings. Example usage in your Flutter code:
import 'src/rust/<project_name>_generated.dart';
import 'src/rust/api/simple.dart';
Future<void> main() async {
await RustLib.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
body: Center(
// `greet` is a function inside `simple.dart`' -> open rust folder and find the `simple.rs` to modify it
child: Text(
'Action: Call Rust `greet("Tom")`\nResult: `${greet(name: "Tom")}`'),
),
),
);
}
}
Conclusion:
The quickstart approach is perfect for new projects or rapid prototyping. It avoids manual setup and provides a working scaffold out of the box. You simply add Rust code, regenerate bindings, and run Flutter. This makes it the best choice for quickly validating ideas, creating demos, or writing tutorials without worrying about low-level build details.
Top comments (0)