利用Rust与Flutter开发一款小工具

Posted 唯鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用Rust与Flutter开发一款小工具相关的知识,希望对你有一定的参考价值。

1.起因

起因是年前看到了一篇Rust + iOS & Android|未入门也能用来造轮子?的文章,作者使用Rust做了个实时查看埋点的工具。其中作者的一段话给了我启发:

无论是 LookinServer 、 Flipper 等 Debug 利器,还是 Flutter / Web Debug Tools,都是在电脑上调试 App。那我们也可以用类似的方式,把实时埋点数据显示在电脑上,不再局限于同一块屏幕。

我司目前的埋点走查是在测试盒子中有一个埋点查看页面,Debug包在数据上报的同时会将信息临时保存起来。当进入这个页面时会以列表的形式展示出来。并且iosandroid的页面展示和使用方式也略有不同。

后面我觉得这样进入退出页面查看不方便,就将页面改成了悬浮窗。虽然方便了一些,但是也发现了新的问题:

  • 手机上屏幕大小有限,悬浮窗只有屏幕的一半,可展示信息有限。
  • 悬浮窗会遮挡页面,有时不便于点击页面上的按钮。

刚好前阵子升级了手机系统到Android 13,发现log在控制台都打印不出来了(后面发现App适配到13就正常了。。)。所以有了一个想法,使用Rust通过WebSocket进行数据发送,使用Flutter实现服务端接收App发送的信息并显示出来。

当然了,如果我们的应用是flutter写的,可以直接使用Dart的ffi来直接调用Rust函数。这个我后面有时间会单独写一篇来分享。

2.实现

之所以选择RustFlutter是看中它们的跨平台能力。使用Rust进行WebSocket数据发送,就不用Android和iOS端去重复开发这个功能,只需要简单调用即可,并且Rust有许多开箱即用的库。

Flutter的跨平台能力就更不用说了。比如这个小工具我就可以一套代码输出Windows和macOS两个平台的安装包,保证接收端逻辑和UI的一致。

发送端

Rust部分

关于Rust库的打包以及双端的使用可以看我上一篇分享的Rust库交叉编译以及在Android与iOS使用。这里主要说一下具体的实现代码。

首先是添加WebSocket 库 ws-rs依赖到Cargo.toml文件:

[dependencies]
ws = "0.9.2"
# 全局的静态变量
lazy_static = "1.4.0"

实现代码如下:

use std::collections::HashMap;
use std::sync::Mutex;
use std::ffi::CStr, os::raw::c_char;
use ws::connect, Handler, Sender, Handshake, Result, Message, CloseCode, Error;
use ws::util::Token;
#[macro_use]
extern crate lazy_static;

lazy_static! 
    static ref DATA_MAP: Mutex<HashMap<String, Sender>> = 
        let map: HashMap<String, Sender> = HashMap::new();
        Mutex::new(map)
    ;


struct Client 
    sender: Sender,
    host: String,


impl Handler for Client 
    fn on_open(&mut self, _: Handshake) -> Result<()> 
        DATA_MAP.lock().unwrap().insert(self.host.to_owned(), self.sender.to_owned());
        Ok(())
    

    fn on_message(&mut self, msg: Message) -> Result<()> 
        println!("<receive> ''. ", msg);
        Ok(())
    

    fn on_close(&mut self, _code: CloseCode, _reasonn: &str) 
        DATA_MAP.lock().unwrap().remove(&self.host);
    

    fn on_timeout(&mut self, _event: Token) -> Result<()> 
        DATA_MAP.lock().unwrap().remove(&self.host);
        self.sender.shutdown().unwrap();
        Ok(())
    

    fn on_error(&mut self, _err: Error) 
        DATA_MAP.lock().unwrap().remove(&self.host);
    

    fn on_shutdown(&mut self) 
        DATA_MAP.lock().unwrap().remove(&self.host);
    



#[no_mangle]
pub extern "C" fn websocket_connect(host: *const c_char) 
    let c_host = unsafe  CStr::from_ptr(host) .to_str().unwrap();
    if let Err(err) = connect(c_host, |out| 
        Client 
            sender: out,
            host: c_host.to_string(),
        
    ) 
        println!("Failed to create WebSocket due to: :?", err);
    


#[no_mangle]
pub extern "C" fn send_message(host: *const c_char, message: *const c_char) 
    let c_message = unsafe  CStr::from_ptr(message) .to_str().unwrap();
    let c_host = unsafe  CStr::from_ptr(host) .to_str().unwrap();
    let binding = DATA_MAP.lock().unwrap();
    let sender = binding.get(&c_host.to_string());
    
    match sender 
        Some(s) => 
            if s.send(c_message).is_err() 
                println!("Websocket couldn't queue an initial message.")
            ;
         ,
        None => println!("None")
    


#[no_mangle]
pub extern "C" fn websocket_disconnect(host: *const c_char) 
    let c_host = unsafe  CStr::from_ptr(host) .to_str().unwrap();
    DATA_MAP.lock().unwrap().remove(&c_host.to_string());

简单实现了连接,发送,断开连接三个方法。思路是连接成功后会将发送结构体(Sender)保存在Map中,每次发送时先检查是否连接再发送。这样也就实现了连接多台设备,一对多发送的功能。

Android还需要添加对应的JNI方法:

#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android 
    extern crate jni;

    use self::jni::objects::JClass, JString;
    use self::jni::JNIEnv;
    use super::*;

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_sendMessage(
        env: JNIEnv,
        _: JClass,
        host: JString,
        message: JString,
    ) 
        send_message(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
            env.get_string(message)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_connect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) 
        websocket_connect(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_disconnect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) 
        websocket_disconnect( 
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    

至此,发送端部分完成。打包集成进项目就可以使用了。

Android部分

Android端调用代码如下:

public class EventLogUtils 

    static 
        System.loadLibrary("event_log_kit");
    

    private static native void sendMessage(final String host, final String message);
    private static native void connect(final String host);
    private static native void disconnect(final String host);

    private static List<String> addressList = null;

    public static List<String> getAddressList() 
        return addressList;
    

    /**
     * 保存 IP 地址,传空时断开所有连接
     */
    public static void saveAddress(String address) 
        if (TextUtils.isEmpty(address)) 
            if (addressList != null) 
                for (String url : addressList) 
                    disconnect(url);
                
            
            addressList = null;
            return;
        
        // 多个地址逗号隔开
        if (address.contains(",")) 
            addressList = new ArrayList<>(Arrays.asList(address.split(",")));
         else 
            addressList = new ArrayList<>();
            addressList.add(address);
        

        for (String url : addressList) 
            // 子线程调用,可替换为其他方案,这里使用了线程池
            Executor.getExecutor().getExecutorService().submit(new Runnable() 
                @Override
                public void run() 
                    // 循环,如果意外断开,自动重连
                    while (addressList != null) 
                        connect("ws://" + url);
                    
                    // 工具连接彻底断开
                
            );
        
    

    /**
     * 发送信息
     */
    public static void sendMessage(String message) 
        if (addressList == null) 
            return;
        
        for (String url : addressList) 
            sendMessage("ws://" + url, message);
        
    


代码也比较简单,连接方法在子线程调用,如果发现连接断开会自动重连。

iOS部分就不具体说明了,实现思路一样的。

接收端

首先是发送数据的定义,发送的是json格式字符串。定义的主要参数如下:

class EventLogEntity 
  /// event/log
  String type = '';
  /// 事件名称或log tag
  String? name;
  /// 手机型号
  String? deviceModel;
  /// 时间戳
  int time = 0;
  String data = '';

  ...

  • type:用于区分数据类型,目前分为埋点事件与log。
  • name:事件名称或log tag,用于数据的筛选。
  • deviceModel:设备名用于区分数据来源,如果有多个设备同时发送数据可以便于分类。
  • time:时间戳,用于数据排序。

其他参数可以根据自己的需求添加,比如log的等级,数据展示时展开或者收起。

UI组件我使用了fluent_ui,它提供了原生Windows应用风格的组件,比较适合桌面端程序。状态管理使用flutter_riverpod

具体的代码实现就不多说了,主要说一下核心的数据接收部分。

// https://doc.xuwenliang.com/docs/dart-flutter/2499
class WebSocketManager

  HttpServer? requestServer;

  Future startWebSocketListen() async 
    final String ip = '192.168.31.232';
    final String port = '51203';
    stopWebSocketListen();
    //HttpServer.bind(主机地址,端口号)
    requestServer = await HttpServer.bind(ip, int.parse(port)).catchError((error) 
      debugPrint('bind error: $error');
    );
    await for(HttpRequest request in requestServer!) 
      serveRequest(request).catchError((error)
        debugPrint('listen error: $error');
      );
    
  

  void stopWebSocketListen() 
    requestServer?.close();
    requestServer = null;
  

  Future serveRequest(HttpRequest request) 
    //判断当前请求是否可以升级为WebSocket
    if (WebSocketTransformer.isUpgradeRequest(request)) 
      //升级为webSocket
      return WebSocketTransformer.upgrade(request).then((webSocket) 
        //webSocket消息监听
        webSocket.listen((msg) async 
          debugPrint('listen:$msg');
		  if (webSocket.closeCode == null) 
            // 这里可以回复客户端消息
            webSocket.add('收到');
          
          // 可以在这里解析数据,刷新页面
		  ...
        );
      );
     else 
      return Future(());
    
  

然后为了便于使用,避免使用者自己查询填写ip,我们需要获取当前设备ip地址:

  Future<String> getDeviceIp() async 
    String ip = "";
    if (!kIsWeb) 
      for (var interface in await NetworkInterface.list()) 
        for (var address in interface.addresses) 
          ip = address.address;
        
      
    
    return ip;
  

端口可以给个默认值或者自己随便输入一个,然后可以用shared_preferences插件保存用户配置。下次启动时就自动连接了。

手机端可以实现一个输入连接地址的页面,输入电脑端的ip和端口号后就可以发送数据了。或者扫描二维码连接。

3.成果展示

目前实现功能如下:

  • 可同时接收多台设备发送数据,数据按机型名称分类展示。
  • 数据的筛选,搜索(关键字高亮)。
  • 搜索记录的保存。
  • json数据格式化展示。


因为小工具在公司内部使用,所以就不开源完整的代码了。有了文章中的核心代码,你可以根据自己的需求实现。也不必局限于这些功能,你完全可以通过Rust和Flutter的跨平台能力开发更多功能,本篇也只是抛砖引玉。

如果本篇对你有所启发帮助,不妨点赞支持一下。如果你有好的想法,也欢迎评论交流。

4.参考

以上是关于利用Rust与Flutter开发一款小工具的主要内容,如果未能解决你的问题,请参考以下文章

利用低代码从0到1开发一款小程序

利用Git Diff比较Excel-推荐一款小工具

利用Git Diff比较Excel-推荐一款小工具

利用Git Diff比较Excel-推荐一款小工具

利用Git Diff比较Excel-推荐一款小工具

小程序如何开发属于自己的一款小程序