现在该计划已在我司一切 Flutter 项目中运用。

GitHub 地址

通常 Flutter 项目涉及网络恳求,就会用到 JSON 转目标,看网上的计划要么 Editor/IDE 工具生成,要么是用网页生成……总归便是要做一件很钢笔又重复的事,本着看闹热不嫌事大的心态,我决议用 Rust 来处理这桩事。

引入 frb

首要作用是让 Dart 调用 Rust 来完结反序列化作业,在此之前先把这个插件用上flutter_rust_bridge。这个插件能够让 Flutter 无缝调用 Rust(根底原理是经过 CLI 生成 Dart 跟相应的 FFI 绑定),好处是能够把一些 Rust 完成得库包装一层给 Flutter 调用,而且操作十分简略,省去了自己配置/编译/构建的一系列工程问题,开箱即用。
首先是装置该插件的 CLI,根据 文档 的说法直接(写这篇东西的时候仍是 ^2.0.0-dev.32,尽管现在还处于 dev 版,可是现已适当可用了)

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

装置完毕后,假如现已有现成的 Flutter 项目,则能够直接经过

flutter_rust_bridge_codegen integrate

完结整合,或者直接经过

flutter_rust_bridge_codegen create your_app_name

来创建整合好该插件的项目。

用 Rust 反序列化生成 Dart 目标

根底配置

假设现在来创建一个简略的项目

flutter_rust_bridge_codegen create deserialize_demo

等候命令履行完毕后,来看一下目录结构长啥样

tree -L 1
.
├── README.md
├── analysis_options.yaml
├── android
├── deserialize_demo.iml
├── flutter_rust_bridge.yaml
├── integration_test
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── rust
├── rust_builder
├── test
├── test_driver
├── web
└── windows
13 directories, 6 files

咱们来要点重视 lib 跟 rust 文件夹,

cd lib && tree -L 3
.
├── main.dart
└── src
    └── rust
        ├── api
        ├── frb_generated.dart
        ├── frb_generated.io.dart
        └── frb_generated.web.dart
4 directories, 4 files
-------------------------------------------------------
cd rust && tree -L 3
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── api
    │ ├── mod.rs
    │ └── simple.rs
    ├── frb_generated.io.rs
    ├── frb_generated.rs
    ├── frb_generated.web.rs
    └── lib.rs
3 directories, 8 files

能够看出创建项目后,会生成一个根底的 Rust 项目,同时也把 Rust 生成了对应的 Dart 跟 FFI 绑定的代码。
已然 Rust 这边是一个正常的项目,那意味着咱们能够直接写一个反序列化的函数导出给 Dart 这边调用,先把 Rust 的 serde 库引入进来

[package]
name = "rust_lib_deserialize_demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.0.0-dev.32"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"

然后把 simple.rs 的代码调整一下,记住保留一下原先的 init_app 函数,由于现在没有啥需求自界说的一些初始行为,我的建议是新建个文件(比如 init.rs)

文件结构变成

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── api
    │ ├── init.rs
    │ ├── mod.rs
    │ └── simple.rs
    ├── frb_generated.io.rs
    ├── frb_generated.rs
    ├── frb_generated.web.rs
    └── lib.rs

再把内容给誊上去

// init.rs
#[flutter_rust_bridge::frb(init)]
pub fn init_app() {
  // Default utilities - feel free to customize
  flutter_rust_bridge::setup_default_user_utils();
}

处理后端呼应的数据

现在咱们假设后端呼应给咱们的是如下这段 JSON,假如没有过错时 err 为 null,没有数据时 data 为 null

{
    "err": null,
    "data": null
}

然后就能够用 Rust 界说呼应结构

#[derive(Debug)]
pub struct Void {}
#[derive(Debug)]
pub struct Response {
  pub err: Option<Void>,
  pub data: Option<Void>,
}

咱们还得完成一下经过字符串转化的函数,可是与此同时,咱们还得处理意外状况,比如后端接口现已有变化了,回来的数据跟你的结构现已不匹配,这就涉及到 Rust 的过错处理,咱们先把正常状况的函数写出来,注意 derive 的变化,以及咱们直接经过 unwrap 解包裹

#[derive(Debug, Deserialize)]
pub struct Void {}
#[derive(Debug, Deserialize)]
pub struct Response {
  pub err: Option<Void>,
  pub data: Option<Void>,
}
impl Response {
  pub fn from(json: String) -> Response {
    serde_json::from_str::<Response>(&json).unwrap()
  }
}

先来界说一个专门表达过错的枚举类型,为啥要这么费事,首要是考虑用 Rust 可能不止做反序列化这一件事,可能未来项目中会用于处理一些其他方面的事(比如对接算法工程师的算法,或者进行一些 I/O 无关但吃 CPU 的操作),个么一致称号这一类过错叫 FFIError,具体哪一类便是对应的枚举值

#[derive(Debug)]
pub enum FFIError {
  JsonError(serde_json::Error),
}

现在光界说枚举还不行,还要给枚举完成三个 trait,std::fmt::Display 是用来输出过错,From<serde_json::Error> 是用来传递 serde_json 转化的过错,至于 SseEncode 是给 frb 这个库运用的,由于它不认识 FFIError 这个类型

impl std::fmt::Display for FFIError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match *self {
      FFIError::JsonError(ref cause) => write!(f, "{}", cause),
    }
  }
}
impl From<serde_json::Error> for FFIError {
  fn from(value: serde_json::Error) -> Self {
    FFIError::JsonError(value)
  }
}
impl SseEncode for FFIError {
  fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
    <String>::sse_encode(format!("{:?}", self), serializer);
  }
}

现在把重新回来反序列化的函数,把它改成

pub type Result<T> = std::result::Result<T, FFIError>;
impl Response {
  pub fn from(json: String) -> Result<Response> {
    Ok(serde_json::from_str::<Response>(&json)?)
  }
}

咱们做了两桩事,一桩是类型别名了一个 Result,另一桩是干掉了 unwrap。
现在咱们来处理另一桩事,之前提到过咱们

当前项目中,一段 JSON 假如没有过错时 err 为 null,没有数据时 data 为 null

个么便是讲,err 不为空的状况也得处理,假设这是后端界说的过错时的 JSON 表示

{
    "err": {
        "code": 403,
        "msg": "权限缺乏"
    },
    "data": null
}

于是咱们写一个结构体,同时把之前 code 的类型改一下

#[derive(Debug, Deserialize)]
pub struct RespErr {
  pub code: i32,
  pub msg: String,
}
#[derive(Debug, Deserialize)]
pub struct Response {
  pub err: Option<RespErr>,
  pub data: Option<Void>,
}

当然这仅仅个比如,还有对应的 data 数据也要处理,仅仅把 Void 结构体改成相应的嵌套结构即可,而假如不存在 data 的状况,Void 就拿来占位。记住写完之后履行下生成指令

flutter_rust_bridge_codegen generate

履行完之后,要点重视 Flutter 的 lib 文件夹,它现在变成了,能看到多出了 init.dart 跟 simple.dart,
也便是别离对应 init.rs 跟 simple.rs

├── lib
│ ├── main.dart
│ └── src
│     └── rust
│         ├── api
│         │ ├── init.dart
│         │ └── simple.dart
│         ├── frb_generated.dart
│         ├── frb_generated.io.dart
│         └── frb_generated.web.dart

现在是完整的 simple.rs 的代码

use serde::Deserialize;
use std::result;
use crate::frb_generated::SseEncode;
#[derive(Debug, Deserialize)]
pub struct RespErr {
  pub code: i32,
  pub msg: String,
}
#[derive(Debug, Deserialize)]
pub struct Void {}
pub type Result<T> = result::Result<T, FFIError>;
#[derive(Debug)]
pub enum FFIError {
  JsonError(serde_json::Error),
}
impl std::fmt::Display for FFIError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match *self {
      FFIError::JsonError(ref cause) => write!(f, "{}", cause),
    }
  }
}
impl From<serde_json::Error> for FFIError {
  fn from(value: serde_json::Error) -> Self {
    FFIError::JsonError(value)
  }
}
#[derive(Debug, Deserialize)]
pub struct Response {
  pub err: Option<RespErr>,
  pub data: Option<Void>,
}
impl Response {
  pub fn from(json: String) -> Result<Response> {
    Ok(serde_json::from_str::<Response>(&json)?)
  }
}
impl SseEncode for FFIError {
  fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
    <String>::sse_encode(format!("{:?}", self), serializer);
  }
}

结合 Dio 运用

上面仅仅讲这玩意怎样写,肯定有人会有疑问,为啥要这么费事,其实仅仅初期作业会比较多,后边这些套路模板只要写一遍,唯一会变的便是每个呼应有过错或者有数据的状况要随机应变一下,比如像 Response 跟 Void 这种便是要根据实践事务改变的结构体。
现在来结合 Dio 运用,首先肯定是给项目添加 Dio

flutter pub add dio

然后写个简略的 NodeJS 程序模仿下回来 JSON 的接口,本来想写 PHP 的,由于用 PHP 写得代码比 NodeJS 更少,后来发觉我现在这个设备没装 PHP 环境

const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
  const jsonData = {
    err: null,
    data: null
  };
  // const jsonData = {
  //   err: {
  //     code: 403,
  //     msg: "权限缺乏"
  //   },
  //   data: null
  // };
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify(jsonData));
});
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

写完直接用 NodeJS 跑起来,然后改改 Flutter 的 main.dart,由于是示例项目,直接在 main.dart 改就行了

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:deserialize_demo/src/rust/api/simple.dart' as simple;
import 'package:deserialize_demo/src/rust/frb_generated.dart';
Future<void> main() async {
  await RustLib.init();
  runApp(const MyApp());
}
class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
  final _text = ValueNotifier("");
  String get text => _text.value;
  set text(String newValue) {
    if (newValue == text) {
      return;
    }
    _text.value = newValue;
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: () async {
            final options = Options(responseType: ResponseType.plain);
            final res =
                await Dio().post("http://127.0.0.1:3000", options: options);
            final result = await simple.Response.from(json: res.data);
            if (result.err == null) {
              text = "None";
            } else {
              text = result.err!.msg;
            }
          },
          child: const Icon(Icons.send),
        ),
        appBar: AppBar(title: const Text('JSON Deserialize')),
        body: Center(
          child: ValueListenableBuilder(
            valueListenable: _text,
            builder: (context, v, _) => Text(v),
          ),
        ),
      ),
    );
  }
}

能够别离测试一下 err 为空的状况跟 err 不为空的状况,总归便是现在咱们现已完成了自动反序列化的操作了。

from 办法简化

咱们还发觉,完成从字符串反序列化的函数也是模板,这怎样能忍,直接派生宏走起

cargo init duplicated-derive

改改 Cargo.toml

[package]
name = "duplicated-derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
proc-macro2 = "1.0"
syn = { version = "2", features = ["full"] }

完成下宏

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Duplicated)]
pub fn derive_duplicated(i: TokenStream) -> TokenStream {
  fn generate(syntax_tree: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let struct_name = syntax_tree.ident;
    Ok(quote::quote! {
      impl #struct_name {
          pub fn from(json: String) -> Result<#struct_name> {
              Ok(serde_json::from_str::<#struct_name>(&json)?)
          }
      }
    })
  }
  let syntax_tree = parse_macro_input!(i as DeriveInput);
  match generate(syntax_tree) {
    Ok(st) => st.into(),
    Err(e) => e.to_compile_error().into(),
  }
}

然后把 simple.rs 某段代码改掉,后续有同类(进口)结构体,都能够用这个派生宏

#[derive(Debug, Deserialize, duplicated_derive::Duplicated)]
pub struct Response {
  pub err: Option<RespErr>,
  pub data: Option<Void>,
}