DEV Community

Cover image for Rust + Flutter: How to Build Fast, Safe, Cross-Platform Mobile Apps
abibeh
abibeh

Posted on

Rust + Flutter: How to Build Fast, Safe, Cross-Platform Mobile Apps

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 under android/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 use DynamicLibrary.process() or open the framework.
  • Tools that make life much easier (optional but recommended): cargo-ndk (Android), cargo-lipo (iOS), and cbindgen (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 over cargo-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

  1. Manual dart:ffi approach (this tutorial focuses on this) — you write extern "C" APIs in Rust, build platform libraries, and call them from Dart via dart:ffi. This is generic and minimal-dependency; great for an educational article.

  2. Use a codegen bridge such as flutter_rust_bridge or uniffi — 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 for flutter_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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • #[no_mangle] keeps the symbol name stable.
  • extern "C" ensures C ABI compatibility (necessary for dart:ffi).
  • For strings we hand out an allocated char* and also export a free 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
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

Verify:

echo $ANDROID_NDK_HOME
ls $ANDROID_NDK_HOME
Enter fullscreen mode Exit fullscreen mode

4. Build for Android (recommended: use cargo-ndk)

Install cargo-ndk if you like:

cargo install cargo-ndk
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

On modern Apple Silicon macs you may need to build for aarch64-apple-ios-sim as well. If cargo-lipo does not produce all the slices you need, build the targets individually (--target) and pass both .a files to xcodebuild.

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
Enter fullscreen mode Exit fullscreen mode

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 and Copy items if needed).
  • In the target Build PhasesLink 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');

  }
}
Enter fullscreen mode Exit fullscreen mode

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)),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
Enter fullscreen mode Exit fullscreen mode

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 or flutter build apk should package the .so files from android/app/src/main/jniLibs/* automatically.
  • If your binary isn't found at runtime check adb logcat for UnsatisfiedLinkError and double-check the path & ABI.

iOS

  • flutter build ios then open ios/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. Use nm (mac/linux) or readelf -s to inspect exported symbols.
  • Architecture mismatch: Ensure you built the library for the ABI the device expects. Use file or lipo -info on built binaries.
  • iOS simulator on Apple Silicon: you may need to produce aarch64 slices for the simulator. cargo-lipo + building aarch64-apple-ios-sim can help.
  • Dart FFI type mismatch: Ensure C types map to the correct Dart FFI types (e.g., int32Int32).
  • 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 or uniffi 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
Enter fullscreen mode Exit fullscreen mode

2. Create a new Flutter + Rust project

Run the FRB helper to scaffold a full project:

flutter_rust_bridge_codegen create my_app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

3. Run the project

Move into the Flutter directory and run it like any Flutter app:

cd my_app/flutter
flutter run
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

5. Regenerate bindings (new workflow)

Whenever you change your Rust API, run:

flutter_rust_bridge_codegen generate
flutter_rust_bridge_codegen integrate
Enter fullscreen mode Exit fullscreen mode
  • generate → scans api.rs and produces new Dart + Rust glue code
  • integrate → updates the Flutter project with the generated bindings

Then re-run Flutter:

flutter run
Enter fullscreen mode Exit fullscreen mode

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")}`'),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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)