Rinf: Rust in Flutter
repository·main·Indexed 25 days ago
https://github.com/cunarist/rinfA framework for creating performant cross-platform applications using Rust for native business logic and Flutter for the UI layer. Rinf provides a type-safe, event-driven communication bridge between Dart and Rust via native FFI, supporting Linux, Android, Windows, macOS, iOS, Web, and experimental eLinux. It includes a CLI for scaffolding templates, generating Dart classes from Rust signals, and managing project configuration.
What's inside Rinf
- Rinf is a framework designed to combine Rust's high-performance native business logic with Flutter's flexible and beautiful GUI. It allows developers to write Rust code for heavy computations or complex logic and interact with it from a Flutter application using a type-safe, event-driven communication layer. All communication occurs through native FFI, ensuring high efficiency without the overhead of webviews or web servers.
Understand the Rinf architecture and design principles
mainRinf is designed for building cross-platform applications by combining Rust for business logic and Flutter for the UI layer.
Key architectural principles:
- Separation of Concerns: All business logic should be written in Rust. If your business logic is primarily in Dart, Rinf may not be the appropriate solution.
- Communication Mechanism: Instead of direct function calls, Rinf uses a two-way, stream-based message-passing mechanism. This decouples the Rust business logic from the Flutter UI.
- State Management Pattern:
- Rust side: It is recommended to use the actor model. Actors should hold the application state.
- Flutter side: Use tree-based state management (such as
InheritedWidgetorprovider) to propagate view data. Flutter widgets should only receive the specific view data required to update the UI.
Perform minor Rinf upgrades
mainWhen performing minor upgrades, ensure that the Rinf versions specified inpubspec.yaml(Flutter side) andnative/hub/Cargo.toml(Rust side) are identical to maintain compatibility.Build and upload precompiled binaries via GitHub Actions
mainYou can automate the precompilation and uploading of binaries using a GitHub Action. The action requires two secrets:
RELEASE_PRIVATE_KEY(for signing) andRELEASE_GITHUB_TOKEN(for uploading to releases).Use the
dart run build_tool precompile-binariescommand.Key Arguments:
--manifest-dir: Path to the Rust manifest directory.--repository: The repository identifier.--target <rust-triple>: (Optional) Override to build for a specific target.--android-sdk-location: Required for Android builds.--android-ndk-version: Required for Android builds.--android-min-sdk-version: Required for Android builds.
on: push: branches: [main] name: Precompile Binaries jobs: Precompile: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - macOS-latest - windows-latest steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v1 - name: Install GTK if: (matrix.os == 'ubuntu-latest') run: sudo apt-get update && sudo apt-get install libgtk-3-dev - name: Precompile if: (matrix.os == 'macOS-latest') || (matrix.os == 'windows-latest') run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions working-directory: super_native_extensions/cargokit/build_tool env: GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} - name: Precompile (with Android) if: (matrix.os == 'ubuntu-latest') run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions --android-sdk-location=/usr/local/lib/android/sdk --android-ndk-version=24.0.8215888 --android-min-sdk-version=23 working-directory: super_native_extensions/cargokit/build_tool env: GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }}Avoid panicking in Rust code
mainTo ensure predictable application behavior and prevent hangs on the web platform (
wasm32-unknown-unknown), do not use panicking code like.unwrap()or.expect(). Instead, use the idiomaticResult<T, E>type to handle errors gracefully. Rinf expects business logic to be handled in Rust, and panics cannot be caught on the web, which may cause Flutter callers to wait indefinitely.fn good() -> Result<(), SomeError> { let option = get_option(); let value_a = option.ok_or(SomeError)?; let result = get_result(); let value_b = result?; Ok(()) }Migrate from Rinf 6 to 7
mainUpgrading from version 6 to 7 involves several API and workflow changes:
- Rust Main Function: Explicitly bind the
mainfunction with an async runtime (e.g.,tokio) and await therinf::dart_shutdown()future. - Protobuf Annotations: Remove the
RINF:prefix from Protobuf message annotations. Change[RINF:DART-SIGNAL]to[DART-SIGNAL]. - Module Imports:
- In Dart, import messages from the root
generatedmodule instead of inner modules. - In Rust, import messages from the root
generatedmodule.
- In Dart, import messages from the root
- Web Server: Use the
rinf serverCLI command to run the Flutter web server with the required arguments.
// Rust main function requirement [tokio::main] async fn main() { // ... rinf::dart_shutdown().await; }// Protobuf annotation change // [DART-SIGNAL] message SomeMessage {}// Dart import change import 'generated.dart';# Run Flutter web server rinf server- Rust Main Function: Explicitly bind the
Run and build for the web
mainWeb support requires a manual WebAssembly (Wasm) build step from Rust before running or building the Flutter app. Rinf uses
wasm-bindgenandwasm-packwith thewebtarget internally.Development (Serving the app)
To serve the web application, use
rinf wasmto generate the necessary Wasm modules, then run Flutter with specific cross-origin headers. Rinf provides a helper commandrinf serverthat prints the full command required for development.Production (Building the app)
To build an optimized release version, use
rinf wasm --releasefollowed by the Flutter build command.Deployment Requirements
When deploying to a web server, you MUST configure the server to include the following HTTP headers to enable
SharedArrayBuffersupport:cross-origin-opener-policy:same-origincross-origin-embedder-policy:require-corp
Additionally, ensure your server is configured to serve
.wasmfiles with theapplication/wasmMIME type.# 1. Build Wasm and run for development rinf wasm flutter run --web-header=cross-origin-opener-policy=same-origin --web-header=cross-origin-embedder-policy=require-corp # 2. Build optimized Wasm and build for production rinf wasm --release flutter build webSend signals from Rust to Dart
mainTo stream data or notify Dart from Rust, follow these steps:
- Define the Signal in Rust: Create a struct in your Rust crate and annotate it with
#[derive(Serialize, RustSignal)]. - Generate Dart Code: Run the Rinf CLI to generate the corresponding Dart classes.
rinf gen - Send the Signal in Rust: Use the generated
.send_signal_to_dart()method on your signal instance. - Receive the Signal in Dart: Use the generated
.rustSignalStreamproperty within aStreamBuilderwidget to react to incoming signals.
// 1. Define in Rust #[derive(Serialize, RustSignal)] pub struct MyAmazingNumber { pub current_number: i32, } // 3. Send in Rust MyAmazingNumber { current_number }.send_signal_to_dart();// 4. Receive in Dart StreamBuilder( stream: MyAmazingNumber.rustSignalStream, builder: (context, snapshot) { final signalPack = snapshot.data; if (signalPack == null) return Text('Nothing received yet'); final myAmazingNumber = signalPack.message; return Text(myAmazingNumber.currentNumber.toString()); }, )- Define the Signal in Rust: Create a struct in your Rust crate and annotate it with
Preview documentation changes with autobuild
mainTo automatically apply and preview documentation changes as you write them, use
sphinx-autobuildwith thedirhtmlbuilder.uv run sphinx-autobuild source dist --builder dirhtmlConfigure Rust-analyzer for WebAssembly (Wasm) linting
mainBy default, Rust-analyzer runs in native mode. To enable type checking and linting for the web target (
wasm32-unknown-unknown), create a.cargo/config.tomlfile and set the target. You must restart the Rust language server for changes to take effect.# .cargo/config.toml [build] # Uncomment the line below to switch Rust-analyzer to perform # type checking and linting in webassembly mode, for the web target. # You might have to restart Rust-analyzer for this change to take effect. target = "wasm32-unknown-unknown"Test pure Rust business logic
mainTo test Rust code that does not involve Dart communication, use standard Rust testing patterns. You can use
#[tokio::test]for asynchronous functions and run the tests using the Cargo CLI.#[tokio::test] async fn my_async_test() { let result = async_function().await; assert_eq!(result, 42); } async fn async_function() -> i32 { 42 }cargo testGenerate Dart classes from Rust signals
mainRinf uses signal attributes to implement communication between Dart and Rust. If you modify your Rust signal structs, you must run therinf gencommand to regenerate the corresponding Dart classes.rinf gen