ByteNoteByteNote
Flutter 管画,Rust 管算:Rinf 分形实战
字

字节笔记本

2026年10月5日 · 约 32 分钟读完

Flutter 管画,Rust 管算:Rinf 分形实战

API中转
¥120

一个"拖拽缩放看分形"的小应用,用来演示 Rinf 的核心模式:Flutter 只管画,Rust 只管算,中间靠消息事件沟通。

Rinf 架构与消息流:Flutter 与 Rust 通过消息协作

为什么用分形当 Demo

这个 Demo 做的事对应 Rinf 的什么能力
用户在 Flutter 上拖拽、缩放Dart 发往 Rust 的请求消息
Rust 多线程算每个像素的颜色Rust 后台干活,不卡 UI
Rust 边算边推"已完成 30%"Rust 发往 Dart 的事件流(EventSink)
Rust 算完推一帧像素大块二进制数据传输
Flutter 用 CustomPainter 把像素画出来Dart 侧纯渲染

这是 Rinf 最舒服的形态:事件驱动、后台计算、前台展示。相比常见的下载进度条示例,分形的视觉冲击强得多:算完那一刻,你能亲眼看着一张图从像素里浮现出来。

项目结构

text
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.yaml

1. Rust 侧

native/Cargo.toml

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 自动生成。

rust
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 = ();
}

这里停一下,看清楚三件事:

  1. RenderRequest 是命令:Dart 发给 Rust,让它干活。
  2. RenderProgress 是事件:Rust 边干边喊"我干了 30% 了",Dart 收到就更新进度条。
  3. RenderResult 是结果:Rust 算完把整张图的像素塞过来。

这就是 Actor 模型的味道:Rust 和 Dart 是两个 actor,靠消息协作,谁也不阻塞谁。

native/src/fractal.rs:纯计算内核,不知道 Flutter 存在

注意这个文件完全不 import rinf。这是 Rinf 解耦的精髓:核心算法是个纯 crate,你想拿去喂 CLI、跑服务器、做测试,都行。

rust
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:主循环,把消息和计算粘起来

rust
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

yaml
dependencies:
  flutter:
    sdk: flutter
  rinf: ^7.0.0

lib/main.dart

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、手势与发请求

dart
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:把像素画出来

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. 跑起来

bash
# 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 实时渲染词云,也只是把消息结构体换掉,整套骨架原样可用。

相关文章

分享: