跳到主内容
Flutter
文章阅读

使用flutter_rust_bridge编写rust和dart的组件库

2025/12/1689 次阅读24 分钟

使用flutter_rust_bridge来编写一个aria2下载器

效果预览

image.png

安装基础库


cargo install 'flutter_rust_bridge_codegen@^2.0.0-dev.0'

新建/添加项目


flutter_rust_bridge_codegen create my_app #新建一个
or
flutter_rust_bridge_codegen integrate #现有目录中添加

编辑rust代码

找到rust文件夹,在src/api目录下编写rs代码 编写后插件目录下执行命令flutter_rust_bridge_codegen generate --watch,生成对应的dart代码

附上我写的aria2下载器api

aria.rs


use crate::frb_generated::StreamSink;
use aria2_ws::*;
use flutter_rust_bridge::{frb, DartFnFuture};
use futures::FutureExt;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
// use aria2_ws::*;
// use serde_json::Value;
///新建连接
pub async fn connect_aria2_client(url: String) -> Client {
    let client = Client::connect(&url, None).await.unwrap();
    client
}

///创建配置
pub fn create_aria_task_option(
    header: Option<Vec<String>>,
    all_proxy: Option<String>,
    split: Option<i32>,
    dir: Option<String>,
    out: Option<String>,
    gid: Option<String>,
    r_continue: Option<bool>,
    auto_file_renaming: Option<bool>,
    check_integrity: Option<bool>,
    lowest_speed_limit: Option<String>,
    max_download_limit: Option<String>,
    max_tries: Option<i32>,
    max_connection_per_server: Option<i32>,
    timeout: Option<i32>,
    extra_options: HashMap<String, String>,
) -> TaskOptions {
    TaskOptions {
        header: header,
        split: split,
        all_proxy: all_proxy,
        dir: dir,
        out: out,
        gid: gid,
        r#continue: r_continue,
        auto_file_renaming: auto_file_renaming,
        check_integrity: check_integrity,
        lowest_speed_limit: lowest_speed_limit,
        max_download_limit: max_download_limit,
        max_connection_per_server: max_connection_per_server,
        max_tries: max_tries,
        timeout: timeout,
        extra_options: json!(extra_options).as_object().unwrap().clone(),
    }
}

///添加任务
pub async fn add_aria_task(
    urls: Vec<String>,
    client: &Client,
    options: Option<TaskOptions>,
    position: Option<u32>,
    on_complete: impl Fn() -> DartFnFuture<()> + Send + 'static,
    on_error: impl Fn() -> DartFnFuture<()> + Send + 'static,
) -> Result<String, MyError> {
    let gid = client
        .add_uri(
            urls,
            options,
            position,
            Some(Callbacks {
                on_download_complete: Some(
                    async move {
                        let result = on_complete().await;
                        result
                    }
                    .boxed(),
                ),
                on_error: Some(
                    async move {
                        let result = on_error().await;
                        result
                    }
                    .boxed(),
                ),
            }),
        )
        .await;
    match gid {
        Ok(id) => Ok(id),
        Err(e) => Err(MyError::from(e)),
    }
}

///监听aria消息
pub async fn listen_aria_msg(
    client: &Client,
    sink: StreamSink<MyNotification>,
) -> Result<(), String> {
    let mut not = client.clone().subscribe_notifications();
    flutter_rust_bridge::spawn(async move {
        while let Ok(msg) = not.recv().await {
            let _ = sink.add(MyNotification::from(msg));
        }
    });
    Ok(())
}

///获取任务的状态
pub async fn get_aria_status(client: &Client, gid: String) -> Result<String, MyError> {
    let result = client
        .call_and_wait::<Value>("tellStatus", vec![Value::String(gid)])
        .await;
    match result {
        Ok(r) => Ok(serde_json::to_string(&r).unwrap()),
        Err(e) => {
            println!("call error: {:?}", e);
            Err(MyError::from(e))
        }
    }
}

///通用函数调用
pub async fn call_aria_function(
    client: &Client,
    method: String,
    params: MyValue,
) -> Result<String, MyError> {
    let result = client
        .call_and_wait::<String>(&method, vec![my_value_to_value(params)])
        .await;
    match result {
        Ok(r) => Ok(r),
        Err(e) => {
            println!("error:{:?}", e);
            Err(MyError::from(e))
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub enum MyNotification {
    Aria2 { gid: String, event: MyEvent },
    WebSocketConnected,
    WebsocketClosed,
}

#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub enum MyEvent {
    Start,
    Pause,
    Stop,
    Complete,
    Error,
    /// This notification will be sent when a torrent download is complete but seeding is still going on.
    BtComplete,
}

impl From<Notification> for MyNotification {
    fn from(notification: Notification) -> Self {
        match notification {
            Notification::Aria2 { gid, event } => MyNotification::Aria2 {
                gid,
                event: event.into(),
            },
            Notification::WebSocketConnected => MyNotification::WebSocketConnected,
            Notification::WebsocketClosed => MyNotification::WebsocketClosed,
        }
    }
}

impl From<Event> for MyEvent {
    fn from(event: Event) -> Self {
        match event {
            Event::Start => MyEvent::Start,
            Event::Pause => MyEvent::Pause,
            Event::Stop => MyEvent::Stop,
            Event::Complete => MyEvent::Complete,
            Event::Error => MyEvent::Error,
            Event::BtComplete => MyEvent::BtComplete,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub enum MyValue {
    Bool(bool),
    String(String),
    Array(Vec<MyValue>),
}

pub fn my_value_to_value(my_value: MyValue) -> Value {
    match my_value {
        MyValue::Bool(b) => Value::Bool(b),
        MyValue::String(s) => Value::String(s),
        MyValue::Array(vec) => Value::Array(vec.into_iter().map(my_value_to_value).collect()),
    }
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub enum MyError {
    Aria2 { source: String },
    Parse { value: String, to: String },
    WebsocketIo { source: String },
    Json { source: String },
    WebsocketClosed { message: String },
    ReconnectTaskTimeout { source: String },
}

impl From<Error> for MyError {
    fn from(error: Error) -> Self {
        match error {
            Error::Aria2 { source } => MyError::Aria2 {
                source: format!("{:?}", serde_json::to_string(&source)),
            },
            Error::Parse { value, to } => MyError::Parse { value, to },
            Error::WebsocketIo { source } => MyError::WebsocketIo {
                source: format!("{:?}", source),
            },
            Error::Json { source } => MyError::Json {
                source: format!("{:?}", source),
            },
            Error::WebsocketClosed { message } => MyError::WebsocketClosed { message },
            Error::ReconnectTaskTimeout { source } => MyError::ReconnectTaskTimeout {
                source: format!("{:?}", source),
            },
        }
    }
}

dart端插件封装


import 'dart:async';
import 'dart:convert';

import 'package:aria2_plugin/src/rust/api/aria.dart';
import 'package:flutter/cupertino.dart';

import 'model/aria_detail.dart';

class Aria2Plugin {
  static final Aria2Plugin instance = Aria2Plugin._();

  factory Aria2Plugin() => instance;

  Aria2Plugin._();

  Client? client;
  StreamSubscription<MyNotification>? _stream;

  ///aria2 connection address
  Future<Client> connect(String address) async {
    client ??= await connectAria2Client(url: address);
    return client!;
  }

  ///add download task
  ///[return] gid
  Future<String> addTask(List<String> urls,
      {VoidCallback? onComplete, VoidCallback? onError}) async {
    if (client != null) {
      final opt = await createAriaTaskOption(extraOptions: {});
      final gid = await addAriaTask(
          urls: urls,
          client: client!,
          options: opt,
          onComplete: onComplete ?? () {},
          onError: onError ?? () {});
      return gid;
    }
    throw Aria2Error(message: 'please connect  first');
  }

  ///add listener
  void addListen({ValueChanged<MyNotification>? listen}) {
    closeListen();
    if (client != null) {
      _stream ??= listenAriaMsg(client: client!).listen((event) {
        listen?.call(event);
      });
    }
  }

  ///turn off listening
  void closeListen() {
    if (_stream != null) {
      _stream!.cancel();
      _stream = null;
    }
  }

  ///get details
  Future<AriaDetail?> getInfo(String gid) async {
    if (client != null) {
      final info = await getAriaStatus(client: client!, gid: gid);
      return AriaDetail.fromJson(jsonDecode(info));
    }
    return null;
  }
}

class Aria2Error extends Error {
  final String message;

  Aria2Error({required this.message});

  @override
  String toString() {
    return message;
  }
}

项目中调用插件编写一个简单的下载器


import 'package:aria2_plugin/aria2_plugin.dart';
import 'package:aria2_plugin/model/aria_detail.dart';
import 'package:dd_js_util/dd_js_util.dart';
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:aria2_plugin/src/rust/api/aria.dart';
import 'package:aria2_plugin/src/rust/frb_generated.dart';
import 'package:provider/provider.dart';

Future<void> main() async {
  await RustLib.init();
  runApp(const MyApp());
}

///状态管理
class Model extends ChangeNotifier {
  final gids = <String, MyEvent>{};
  final infos = <String, AriaDetail>{};
  Client? client;
  late Timer _timer;

  //连接
  Future<void> connect(String address) async {
    client = await Aria2Plugin().connect(address);
    Aria2Plugin().addListen(listen: _onListen);
    notifyListeners();
  }

  //添加任务
  Future<void> addTask(String url) async {
    Aria2Plugin().addTask([url]);
  }

  void cancelTimer() {
    if (!gids.containsValue(MyEvent.start)) {
      debugPrint('canceled timer');
      _timer.cancel();
    }
  }

  void startTimer() {
    debugPrint("start timer task");
    _timer = Timer.periodic(const Duration(seconds: 1), _doGetInfo);
  }

  ///监听
  void _onListen(MyNotification value) {
    value.whenOrNull(
      aria2: (gid, event) {
        gids[gid] = event;
        notifyListeners();
        if (event == MyEvent.start) {
          startTimer();
        } else {
          delayFunction(() {
            cancelTimer();
          }, 2000);
        }
      },
    );
  }

  void _doGetInfo(Timer timer) {
    gids.forEach((key, value) {
      if (value case MyEvent.start) {
        Aria2Plugin().getInfo(key).then((r) {
          if (r != null) {
            infos[key] = r;
            notifyListeners();
          }
        });
      }
    });
  }

  void cancel() {
    _timer.cancel();
    client?.dispose();
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _textEditController =
      TextEditingController(text: 'ws://127.0.0.1:6800/jsonrpc');
  final _urlController = TextEditingController(
      text:
          'https://alist.azad.asia/d/189yun_2/%E7%BF%BC%E7%8B%90MAYA%E5%AE%8C%E5%85%A8%E8%87%AA%E5%AD%A6%E5%AE%9D%E5%85%B8%E3%80%90%E5%B7%B2%E6%95%B4%E5%90%8817%E7%AB%A0%E3%80%91/%E7%AC%AC04%E7%AB%A0%EF%BC%9A%E3%80%90%E6%A8%A1%E5%9E%8B%E7%AF%87%E3%80%91NURBS%E5%9F%BA%E7%A1%80.zip');

  final model = Model();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(
            title: const Text('aria2下载器'),
          ),
          body: ChangeNotifierProvider(
            create: (context) => model,
            builder: (context, child) {
              return Consumer<Model>(
                builder: (context, value, child) {
                  final Model(:client, :gids, :infos) = value;
                  return SingleChildScrollView(
                    padding: const EdgeInsets.all(12),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Expanded(
                                child: TextField(
                              controller: _textEditController,
                            )),
                            FilledButton(
                                onPressed: _connect, child: const Text("连接服务"))
                          ],
                        ),
                        Row(
                          children: [
                            Expanded(
                              child: TextField(
                                controller: _urlController,
                                maxLines: 3,
                              ),
                            ),
                            FilledButton(
                                onPressed: client == null ? null : _addTask,
                                child: const Text('添加下载任务'))
                          ],
                        ),
                        Padding(
                          padding: const EdgeInsets.symmetric(
                              vertical: 12, horizontal: 8),
                          child:
                              Text('任务列表', style: context.textTheme.titleLarge),
                        ),
                        ...gids.keys.map((e) {
                          final info = infos[e];
                          return _Info(item: info, event: gids[e]);
                        })
                      ],
                    ),
                  );
                },
              );
            },
          )),
    );
  }

  Future<void> _connect() async {
    final url = _textEditController.text;
    if (url.isNotEmpty) {
      model.connect(url);
    }
  }

  Future<void> _addTask() async {
    final url = _urlController.text;
    model.addTask(url);
  }
}

class _Info extends StatelessWidget {
  final AriaDetail? item;
  final MyEvent? event;
  const _Info({this.item, this.event});

  @override
  Widget build(BuildContext context) {
    final value = event == MyEvent.complete ? 1.0 : item?.percentage ?? 0.01;
    return Card(
      margin: const EdgeInsets.all(12),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(Uri.decodeComponent(
                item?.files.first.path.urlManager.filenameAll ?? '')),
            const SizedBox(
              height: 12,
            ),
            Wrap(
              spacing: 6,
              runSpacing: 6,
              children: [
                //
                Chip(
                    label: Text(
                        '总大小:${ByteModel.create(item?.totalLengthDouble ?? 0.1).format()}')),
                Chip(
                    label: Text(
                        '已下载:${ByteModel.create(item?.completedLengthDouble ?? 0.1).format()}')),
                Chip(label: Text('保存目录:${item?.dir ?? ''}')),
                Chip(label: Text('已下载字节:${item?.completedLength ?? 0}')),
                Chip(label: Text('总字节大小:${item?.totalLength ?? 0}')),
                Chip(
                    label: Text(
                        '下载速度:${ByteModel.create(item?.downloadSpeedDouble ?? 0).format(2)}'))
              ],
            ),
            //
            const SizedBox(
              height: 12,
            ),
            LinearProgressIndicator(
              minHeight: 1,
              borderRadius: BorderRadius.circular(20),
              value: value, // 设置进度值,范围为0.0到1.0
              backgroundColor: Colors.grey, // 设置背景颜色
              valueColor:
                  const AlwaysStoppedAnimation<Color>(Colors.blue), // 设置进度条颜色
            ),
          ],
        ),
      ),
    );
  }
}

返回顶部