Skip to content

FRB Patterns

Flutter Rust Bridge (FRB) generates Dart bindings for Rust types. Choosing the right annotation determines whether Dart or Rust controls construction and mutation.

AnnotationControlMutationPurposeExample
#[frb(non_opaque)]Dart constructsImmutable / Dart mutates fieldsDTO, value type, snapshotPropertySchema, WhiteboardElement
#[frb(opaque)]Rust controlsRust methods via &mut selfContainer with CRUD, handlePropertySchemaList, NotePropertyValues

Use for DTOs, value types, snapshots, and enum variants. Dart can construct these directly and access all public fields.

#[frb(non_opaque)]
#[derive(Clone, Serialize, Deserialize)]
pub struct PropertySchema {
pub key: String,
pub name: String,
pub property_type: PropertyType,
}
#[frb(non_opaque)]
pub enum WhiteboardElement {
Path(PathElement),
Shape(ShapeElement),
Text(TextElement),
// Generates a Dart sealed class with variants
}

Use for containers with CRUD operations, session handles, or any type where Rust must manage the internal state.

#[frb(opaque)]
pub struct PropertySchemaList {
pub schemas: Vec<PropertySchema>,
}
impl PropertySchemaList {
pub fn new() -> Self { /* ... */ } // FRB: factory
pub fn add(&mut self, s: PropertySchema) { /* ... */ } // FRB: mutation
pub fn len(&self) -> usize { /* ... */ } // FRB: query
pub fn all_schemas(&self) -> Vec<PropertySchema> { /* ... */ } // FRB: snapshot
}
#[frb(ignore)]
impl PropertySchemaList {
pub fn get(&self, key: &str) -> Option<&PropertySchema> { /* ... */ }
// Returns a reference -- cannot cross FFI
}

1. #[frb(ignore)] goes on entire impl blocks

Section titled “1. #[frb(ignore)] goes on entire impl blocks”

FRB does not support ignoring individual methods. Split your impl into two blocks: one for FRB-exposed methods, one for ignored methods (typically those returning references).

// Exposed to FRB
impl MyType {
pub fn count(&self) -> usize { self.items.len() }
pub fn snapshot(&self) -> Vec<Item> { self.items.clone() }
}
// Hidden from FRB
#[frb(ignore)]
impl MyType {
pub fn get(&self, idx: usize) -> Option<&Item> { self.items.get(idx) }
pub fn iter(&self) -> impl Iterator<Item = &Item> { self.items.iter() }
}

2. Only crates/api uses #[frb(...)] attributes

Section titled “2. Only crates/api uses #[frb(...)] attributes”

All other crates use doc comment syntax instead. This keeps the flutter_rust_bridge dependency confined to the API crate.

// In crates/api -- attribute syntax
#[frb(non_opaque)]
pub struct ApiDto { /* ... */ }
// In crates/core, crates/session, etc. -- doc comment syntax
/// flutter_rust_bridge:non_opaque
pub struct CoreDto { /* ... */ }
/// flutter_rust_bridge:sync
pub fn get_version() -> String { /* ... */ }
/// flutter_rust_bridge:ignore
impl InternalType { /* ... */ }

This produces invalid #[frb(frb)] during codegen and crashes the code generator. Always use the specific annotation:

// WRONG -- crashes codegen
/// flutter_rust_bridge:frb
// CORRECT
/// flutter_rust_bridge:non_opaque
/// flutter_rust_bridge:sync
/// flutter_rust_bridge:ignore

4. Doc comments do not work on individual methods

Section titled “4. Doc comments do not work on individual methods”

Just as #[frb(ignore)] must go on entire impl blocks, /// flutter_rust_bridge:ignore must also be placed on the impl block, not on individual functions within it.

5. third_party/ in Dart output is auto-generated

Section titled “5. third_party/ in Dart output is auto-generated”

When FRB scans external crates transitively, it places their generated Dart bindings in lib/src/rust/third_party/. This directory is fully managed by codegen — do not edit it manually.

FRB only generates non_opaque Dart classes for types it can see during codegen. If a type is defined in a crate that FRB does not scan, it will be generated as opaque regardless of annotations.

The scanned entry point is crates/api. Types in crates/core are scanned transitively (since api depends on it). Types in crates/session or platform crates need to be re-exported through core or api if they must be non-opaque on the Dart side.