
字节笔记本
2026年10月5日 · 约 32 分钟读完
Flutter 管画,Rust 管算:Rinf 分形实战
一个"拖拽缩放看分形"的小应用,用来演示 Rinf 的核心模式:Flutter 只管画,Rust 只管算,中间靠消息事件沟通。

为什么用分形当 Demo
| 这个 Demo 做的事 | 对应 Rinf 的什么能力 |
|---|---|
| 用户在 Flutter 上拖拽、缩放 | Dart 发往 Rust 的请求消息 |
| Rust 多线程算每个像素的颜色 | Rust 后台干活,不卡 UI |
| Rust 边算边推"已完成 30%" | Rust 发往 Dart 的事件流(EventSink) |
| Rust 算完推一帧像素 | 大块二进制数据传输 |
| Flutter 用 CustomPainter 把像素画出来 | Dart 侧纯渲染 |
这是 Rinf 最舒服的形态:事件驱动、后台计算、前台展示。相比常见的下载进度条示例,分形的视觉冲击强得多:算完那一刻,你能亲眼看着一张图从像素里浮现出来。
项目结构
fractal_explorer/
├── native/ # Rust 侧(Rinf 默认放这)
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # 入口 + 主循环
│ ├── messages.rs # 消息定义(协议)
│ └── fractal.rs # Mandelbrot 计算内核
├── lib/ # Flutter/Dart 侧
│ ├── main.dart
│ ├── app.dart
│ ├── fractal_painter.dart # 用像素画分形
│ └── api/ # Rinf 自动生成的绑定
└── pubspec.yaml1. Rust 侧
native/Cargo.toml
[package]
name = "rust_lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"] # Rinf 要求
[dependencies]
rinf = "7" # 主角
rayon = "1.10" # 并行计算(关键)
serde = { version = "1", features = ["derive"] }native/src/messages.rs:消息协议,整个 Demo 的灵魂
这一文件定义了 Rust 和 Dart 之间"通信用什么信封"。这就是 Rinf 的核心抽象:不需要手写 proto 文件,两个普通结构体实现 Message 接口就是协议,Dart 侧的绑定由 Rinf 自动生成。
use rinf::Message;
use serde::{Deserialize, Serialize};
/// Dart → Rust:请求渲染一帧分形
#[derive(Serialize, Deserialize, Debug)]
pub struct RenderRequest {
pub width: u32, // 画布宽度(像素)
pub height: u32, // 画布高度(像素)
pub center_re: f64, // 视图中心的实部
pub center_im: f64, // 视图中心的虚部
pub scale: f64, // 缩放(数值越小越放大)
pub max_iter: u32, // 最大迭代次数(越大越精细)
}
impl Message for RenderRequest {
type Dart = (); // 不需要 Dart 侧回复
}
/// Rust → Dart:进度事件(边算边推)
#[derive(Serialize, Deserialize, Debug)]
pub struct RenderProgress {
pub rows_done: u32,
pub total_rows: u32,
}
impl Message for RenderProgress {
type Dart = (); // 单向推送
}
/// Rust → Dart:一帧算完了,这是像素数据
#[derive(Serialize, Deserialize, Debug)]
pub struct RenderResult {
pub width: u32,
pub height: u32,
/// RGBA 字节流,长度 = width * height * 4
pub pixels: Vec<u8>,
pub elapsed_ms: u128,
}
impl Message for RenderResult {
type Dart = ();
}这里停一下,看清楚三件事:
RenderRequest是命令:Dart 发给 Rust,让它干活。RenderProgress是事件:Rust 边干边喊"我干了 30% 了",Dart 收到就更新进度条。RenderResult是结果:Rust 算完把整张图的像素塞过来。
这就是 Actor 模型的味道:Rust 和 Dart 是两个 actor,靠消息协作,谁也不阻塞谁。
native/src/fractal.rs:纯计算内核,不知道 Flutter 存在
注意这个文件完全不 import rinf。这是 Rinf 解耦的精髓:核心算法是个纯 crate,你想拿去喂 CLI、跑服务器、做测试,都行。
use rayon::prelude::*;
/// 算单点 Mandelbrot 迭代次数
fn mandelbrot(cx: f64, cy: f64, max_iter: u32) -> u32 {
let mut x = 0.0;
let mut y = 0.0;
let mut iter = 0;
while x * x + y * y <= 4.0 && iter < max_iter {
let xn = x * x - y * y + cx;
y = 2.0 * x * y + cy;
x = xn;
iter += 1;
}
iter
}
/// 迭代次数 → 颜色(RGBA)。简单调色板,够看了。
fn colorize(iter: u32, max_iter: u32) -> [u8; 4] {
if iter == max_iter {
return [0, 0, 0, 255]; // 集合内部 = 黑
}
let t = iter as f64 / max_iter as f64;
// 一个不丑的蓝紫渐变
let r = (9.0 * (1.0 - t) * t * t * t * 255.0) as u8;
let g = (15.0 * (1.0 - t) * (1.0 - t) * t * t * 255.0) as u8;
let b = (8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t * 255.0) as u8;
[r, g, b, 255]
}
/// 并行计算整张图。callback 在每完成 N 行时回调一次(用于推进度)。
pub fn render<F: Fn(u32) + Sync>(
width: u32,
height: u32,
center_re: f64,
center_im: f64,
scale: f64,
max_iter: u32,
on_progress: F,
) -> Vec<u8> {
let aspect = width as f64 / height as f64;
let half_h = scale;
let half_w = scale * aspect;
// 关键:rayon 的 par_chunks_mut 让多核一起算
// 每行算完后回调推进度
let mut pixels = vec![0u8; (width * height * 4) as usize];
pixels
.par_chunks_mut((width * 4) as usize)
.enumerate()
.for_each(|(y, row)| {
let cy = center_im - half_h + (2.0 * half_h * y as f64 / height as f64);
for x in 0..width {
let cx = center_re - half_w + (2.0 * half_w * x as f64 / width as f64);
let iter = mandelbrot(cx, cy, max_iter);
let [r, g, b, a] = colorize(iter, max_iter);
let off = (x * 4) as usize;
row[off] = r;
row[off + 1] = g;
row[off + 2] = b;
row[off + 3] = a;
}
// 每算 20 行报一次进度(同步回调,F 是 Sync)
if y % 20 == 0 {
on_progress((y as u32) + 1);
}
});
pixels
}这一步有两个 Rust 优势的直接体现:
par_chunks_mut:同样的计算逻辑,用 Dart 写只能单线程跑,慢 4 到 8 倍,多核并行是 Rust 的杀手锏。on_progress: F where F: Sync:回调抽成泛型,Send与Sync约束让多线程调用天然安全,Go 想做到这件事要麻烦得多。
native/src/lib.rs:主循环,把消息和计算粘起来
pub mod messages;
pub mod fractal;
use rinf::main;
use std::time::Instant;
#[main]
fn main() {
// 开一个后台线程,持续监听 Dart 发来的 RenderRequest
std::thread::spawn(|| {
for request in messages::RenderRequest::get_stream() {
let req = request.message;
let total_rows = req.height;
let start = Instant::now();
// 算!每 20 行就往 Dart 推一次进度
let pixels = fractal::render(
req.width,
req.height,
req.center_re,
req.center_im,
req.scale,
req.max_iter,
|rows_done| {
messages::RenderProgress {
rows_done,
total_rows,
}
.send_to_dart(); // ← 这就是 Rinf 的 EventSink
},
);
let elapsed_ms = start.elapsed().as_millis();
messages::RenderResult {
width: req.width,
height: req.height,
pixels,
elapsed_ms,
}
.send_to_dart(); // 推最终结果
}
});
}这就是 Rust 侧的全部。 注意 for request in ...::get_stream() 是个无限循环:Rust 启动后就一直等着 Dart 发消息,来一个算一个。send_to_dart() 就是 Rinf 的事件出口,负责往 Dart 推数据。
2. Dart/Flutter 侧
pubspec.yaml
dependencies:
flutter:
sdk: flutter
rinf: ^7.0.0lib/main.dart
import 'package:flutter/material.dart';
import 'package:rinf/rinf.dart';
import 'app.dart';
import 'api/messages.dart'; // Rinf 生成的绑定(来自 messages.rs)
void main() async {
await Rinf.ensureInitialized(); // 必须先初始化 Rust runtime
// 注册 handler,接收 Rust 推来的消息
// 注意:这里全是"被动接收",Dart 不主动调 Rust 函数
Rinf.registerHandler<RenderProgress>(onProgress);
Rinf.registerHandler<RenderResult>(onResult);
runApp(const FractalApp());
}
// 进度更新:Dart 收到 Rust 的事件,刷新进度条
void onProgress(RustResponse<RenderProgress> resp) {
final msg = resp.message!;
progressNotifier.value = msg.rowsDone / msg.totalRows;
}
// 渲染完成:把像素交给 painter 重绘
void onResult(RustResponse<RenderResult> resp) {
final msg = resp.message!;
progressNotifier.value = 1.0;
frameNotifier.value = FractalFrame(
width: msg.width,
height: msg.height,
pixels: Uint8List.fromList(msg.pixels),
elapsedMs: msg.elapsedMs,
);
}lib/app.dart:UI、手势与发请求
import 'package:flutter/material.dart';
import 'package:rinf/rinf.dart';
import 'fractal_painter.dart';
import 'api/messages.dart';
// 两个全局 notifier,把 Rust 的事件喂给 UI
final progressNotifier = ValueNotifier<double>(0);
final frameNotifier = ValueNotifier<FractalFrame?>(null);
class FractalApp extends StatelessWidget {
const FractalApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Rust + Flutter 分形浏览器')),
body: const FractalView(),
),
);
}
}
class FractalView extends StatefulWidget {
const FractalView({super.key});
@override
State<FractalView> createState() => _FractalViewState();
}
class _FractalViewState extends State<FractalView> {
// 视图状态
double _centerRe = -0.5;
double _centerIm = 0.0;
double _scale = 1.2;
static const _maxIter = 256;
@override
void initState() {
super.initState();
_requestRender(); // 首帧
}
// 关键:这就是"发请求给 Rust"。看起来像普通函数调用,
// 但本质是把消息塞进 Rust 那边的 stream。
void _requestRender() {
final size = MediaQuery.of(context).size;
final dpr = MediaQuery.of(context).devicePixelRatio;
RenderRequest(
width: (size.width * dpr).round(),
height: (size.height * dpr).round(),
centerRe: _centerRe,
centerIm: _centerIm,
scale: _scale,
maxIter: _maxIter,
).sendToRust(); // ← 出口
}
void _zoom(double factor, Offset focal) {
setState(() {
_scale *= factor;
_scale = _scale.clamp(1e-15, 4.0);
});
_requestRender();
}
void _pan(Offset delta) {
final size = MediaQuery.of(context).size;
setState(() {
_centerRe -= (delta.dx / size.width) * 2 * _scale *
(size.width / size.height);
_centerIm += (delta.dy / size.height) * 2 * _scale;
});
_requestRender();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onScaleUpdate: (details) {
if (details.pointerCount == 1) {
_pan(details.focalPointDelta);
} else {
_zoom(2 - details.scale, details.focalPoint);
}
},
child: Stack(
children: [
// 分形画布
Positioned.fill(
child: ValueListenableBuilder<FractalFrame?>(
valueListenable: frameNotifier,
builder: (_, frame, __) {
if (frame == null) {
return const Center(child: Text('正在初始化 Rust...'));
}
return CustomPaint(
painter: FractalPainter(frame),
size: Size.infinite,
);
},
),
),
// 进度条 + 耗时
Positioned(
top: 16,
left: 16,
right: 16,
child: ValueListenableBuilder<double>(
valueListenable: progressNotifier,
builder: (_, p, __) => Column(
children: [
LinearProgressIndicator(value: p == 1.0 ? null : p),
ValueListenableBuilder<FractalFrame?>(
valueListenable: frameNotifier,
builder: (_, f, __) => Text(
f == null ? '' : 'Rust 算这帧花了 ${f.elapsedMs} ms',
),
),
],
),
),
),
],
),
);
}
}lib/fractal_painter.dart:把像素画出来
import 'package:flutter/material.dart';
class FractalFrame {
final int width;
final int height;
final Uint8List pixels; // RGBA
final int elapsedMs;
FractalFrame({
required this.width,
required this.height,
required this.pixels,
required this.elapsedMs,
});
}
class FractalPainter extends CustomPainter {
final FractalFrame frame;
FractalPainter(this.frame);
@override
void paint(Canvas canvas, Size size) {
final image = Uint8List.fromList(frame.pixels);
// 把 RGBA 字节流变成可绘制的 ui.Image
// 完整实现里用 decodeImageFromPixels,或
// ui.Image + ImageDescriptor.fromBytes,效率更高
// 这里保留思路,重点在数据流本身
}
@override
bool shouldRepaint(covariant FractalPainter old) =>
old.frame != frame;
}3. 跑起来
# 1. 创建 Flutter 项目并加入 Rinf 依赖
flutter create fractal_explorer
cd fractal_explorer
flutter pub add rinf
# 2. 安装 Rinf 命令行工具,生成 native/ 下的 Rust 模板
cargo install rinf
rinf template
# 3. 装依赖,编译 Rust 侧
flutter pub get
cd native && cargo build && cd ..
# 4. 运行
flutter run -d macos # 或 windows / linux第一次跑你会看到:黑屏,进度条从 0% 走到 100%,一张分形图浮现;你拖一下,Rust 重算,新帧盖上来。这一刻你会真切感受到 Rinf 在做什么。

这个 Demo 教会你 Rinf 的 5 件事
1. 消息就是协议。 整个 Rust 与 Dart 的通信只靠 messages.rs 里三个结构体:没有手写 proto,没有 IDL,绑定由 Rinf 内部自动生成。改协议就是改结构体,改完直接跑。
2. send_to_rust() 与 send_to_dart() 是两个出口。 一切跨语言调用都从这两个 API 过。Dart 不"调用 Rust 函数",而是"发个消息";Rust 不"返回值",而是"回个消息"。
3. 后台线程别忘开。 lib.rs 里必须用 thread::spawn 包住主循环,否则 Rust 会卡住 Dart 的 UI 线程。这是新手遇到的第一个坑。
4. 计算和 UI 解耦。 fractal.rs 里一行 rinf 都没有:它是个纯 Rust 库,可以单独 cargo test,可以喂给命令行工具,也可以喂给服务器。把引擎做成独立 crate,换任何前端都能直接复用。
5. 进度推送是事件流的甜蜜区。 Rust 算一大块数据,边算边推 progress,Dart 的 ValueNotifier 加 ValueListenableBuilder 就能实时刷进度条。这套模式套到下载器、视频转码、AI 推理上,全都一样。
想继续玩
- 导出 PNG:Rust 侧再加一个
SaveRequest消息,把当前像素写盘,完成后send_to_dart通知路径。 - 动图录制:每帧都推,Dart 侧拼成 GIF。
- 对比性能:同一个 Mandelbrot 用 Dart 单线程算一遍,和 Rust rayon 版比耗时,差距通常在 5 到 8 倍,这是 Rust 选型的硬证据。
如果要把 fractal_painter.dart 里省略的高效渲染补完整,参照 Flutter 官方的 decodeImageFromPixels 与 ui.Image API 即可;换一个场景,比如 Rust 跑词频统计、Flutter 实时渲染词云,也只是把消息结构体换掉,整套骨架原样可用。



