文章阅读
使用flutter_rust_bridge编写rust和dart的组件库
2025/12/1689 次阅读24 分钟
使用flutter_rust_bridge来编写一个aria2下载器
效果预览

安装基础库
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), // 设置进度条颜色
),
],
),
),
);
}
}