阅读 370

flutter + gRPC 开发桌面应用

前言:
不知道为啥,对桌面应用开发有一种执念,虽然一直也没开发什么应用出来。相对于 WEB 应用,还是倾向于桌面应用。
闲来无事也调查过很多桌面应用的方案,也是之前愚钝,最近才想到使用 flutter + gRPC 来实现,当然有什么缺点还没深入研究。

跨平台桌面应用技术方案

在 WEB 应用和移动端应用流行的今天,桌面应用渐渐没落。但有些时候,开发个小工具完成一些简单的处理,还是有需求。
现在也出现了很多桌面应用的实现方案。

个人对桌面应用的要求是能跨平台(至少是 Win/Mac),打包小。 于是搜索了下现在的技术方案。

  • WPF: 好像也能跨平台了,不过一直以来对微软的印象不好,所以也不想用 WPF。

  • Electron: 打包文件比较大,小功能的话,整个100M,感觉有些重。

  • Qt: C++技术者可以使用,现在也有各种语言的绑定实现(Go、Rust等)。这个需要深入学习,个人也不会 C++。

  • Python:Python 也有很多 UI 框架,只是个人不太会 Python,而且有些不习惯解释型语言。

  • fyne: go语言的一个 UI 框架,界面有些丑。

  • go-astilectron: go语言+Electron,网上有评论说还有些问题。

  • flutter: 桌面应用还不成熟,感觉还是主要面向移动端,桌面版现在还没发布个正式版。 有些基本的交互组件还欠缺。

上面的这些方案,Electron 先不考虑,Qt感觉略有难度。
个人学过几天 go,项目中使用过 Flutter,所以比较倾向于 Flutter + go 来实现。

之前介绍过用 go-flutter 来做桌面应用,做着做着感觉来回传递接收的地方,也是有些麻烦。

例如:
go-flutter中,go端用于交互的Map的键必须是 interface{} 类型的,传递和接收时还得各种转换,有的时候需要转换两次,或者还需要自己写高级的转换方法。

后来了解 gRPC 的时候,看官方有 go 语言的例子,也有 Dart 的例子。
然后想到用 Flutter 做界面,用 go 来做具体处理,用 gRPC 来通信,不就可以了。
是不是掉进了 Google 的坑里了?
(中间还想过用 Rust 来做具体处理,不过没深入学过,gPRC官网上也不支持 Rust,只有第三方库。)

后来一了解,懂这两者的早就这么用上了。
看网上相关技术文章也不多,所以想简单写个入门的。

gRPC

网上有很多关于 gRPC 的文章,看官网的文档,一些基本的知识就差不多了解了。
gRPC

go 语言的 gRPC 的 Hello World 示例:
grpc-go/examples/helloworld at master · grpc/grpc-go (github.com)

dart 语言的 gRPC 的 Hello World 示例:
grpc-dart/example/helloworld at master · grpc/grpc-dart (github.com)

上面的示例中,分别都是各自语言的 Server 端和 Client 端。
启动 go 语言的Server端 和 dart 语言的 Client 端,理论上就可以通信了。

运行 go 语言的 Hello World 示例

按照官网的顺序配置好 gRPC 环境。
Server 端和 Client 端,分别 运行 go mod tidy 下载依赖。
然后先运行 Server 端,后运行 Client 端,即可看到结果。

image.png

终端的左边是 Server 端命令,右边是 Client 端命令。

运行 dart 语言的 Hello World 示例

按照官网的顺序配置好 gRPC 环境。
打开工程后,需要先下载依赖。
在 VSCode 中打开 pubspec.yaml,点【Get Packages】下载。

image.png

下载完成后,代码文件不再提示错误。

image.png

先运行 Server 端,再运行 Client 端。

Server 端运行命令:
dart bin/server.dart

Client 端运行命令:
dart bin/client.dart

image.png

运行 go 语言的 Server 端 和 dart 语言的 Client 端

go 语言示例的 helloworld.proto:
头部注释略

syntax = "proto3"; option go_package = "google.golang.org/grpc/examples/helloworld/helloworld"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; package helloworld; // The greeting service definition. service Greeter {   // Sends a greeting   rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest {   string name = 1; } // The response message containing the greetings message HelloReply {   string message = 1; } 复制代码

dart 语言示例的 helloworld.proto:
头部注释略

syntax = "proto3"; option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW"; package helloworld; // The greeting service definition. service Greeter {   // Sends a greeting   rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest {   string name = 1; } // The response message containing the greetings message HelloReply {   string message = 1; } 复制代码

上面两个示例中的 proto 文件除了 option 的部分,看上去内容一致,应该能直接通信,先试一下。

启动 go 语言的 Server 端:

image.png

运行 dart 语言的 Client 端:

image.png

还是太乐观了,报错了。

错误内容是:

gRPC Error (code: 12, codeName: UNIMPLEMENTED, message: grpc: Decompressor is not installed for grpc-encoding "gzip", details: [], rawResponse: null, trailers: {}) 复制代码

其中 错误 message 内容是:

grpc: Decompressor is not installed for grpc-encoding "gzip" 复制代码

意思应该是针对grpc-encoding "gzip"的解压缩器未安装。

分析了下通信的代码:

go 语言 Server 端并没有特别指定选项。

s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) 复制代码

dart语言 Client 端请求时在请求选项中指定了压缩方式。

final response = await stub.sayHello(   HelloRequest()..name = name,   options: CallOptions(compression: const GzipCodec()), ); 复制代码

把 dart 语言 Client 端请求的选项去掉。

final response = await stub.sayHello(   HelloRequest()..name = name,   // options: CallOptions(compression: const GzipCodec()), ); 复制代码

再运行就可以成功通信了。

image.png

这里是为了方便,去掉了dart 语言 Client 端请求的选项。
也可以在 go 语言 Server 端加上相应的选项。
Server 端和 Client 端的选项一致就能够正确通信。

flutter + gRPC 开发桌面应用

go 语言和 dart 语言能通过 gRPC 正常通信了,那 flutter + gRPC 也就不远了。

创建 flutter 桌面应用工程

最新版的flutter的默认配置应该能直接运行桌面应用。
如果不能运行,flutter 开发桌面应用的配置可参照下面的文章。
go-flutter开发桌面应用(一)第一个桌面应用 - 掘金 (juejin.cn)

来吧,建个 flutter 工程试一下。

创建好工程后,命令行运行 flutter run,会提示选择运行平台。

MacBook-Pro:hellofluttergrpc xxx$ flutter run Multiple devices found: macOS (desktop) • macos  • darwin-x64     • Mac OS X 10.15.7 19H1419 darwin-x64 Chrome (web)    • chrome • web-javascript • Google Chrome 95.0.4638.54 [1]: macOS (macos) [2]: Chrome (chrome) Please choose one (To quit, press "q/Q"):  复制代码

选择1则以桌面应用方式启动,就能看到熟悉的 flutter 示例界面了。

image.png

添加 gRPC

  1. 添加 grpc 依赖

    pubspec.yaml

      async: ^2.2.0   grpc: ^3.0.2   protobuf: ^2.0.0 复制代码

    添加依赖后需要【Get Packages】下载依赖。

  2. 把上面 gRPC的 dart 语言 HelloWorld 示例中的基于 proto 文件生成的相关 dart 文件复制过来。

    image.png

    实际开发中,应该先编写 proto 文件,然后再基于 proto 文件生成相关 dart 代码。

  3. 在 main.dart 文件中,添加请求处理。

    _MyHomePageState的内容改为如下:
    这里主要把 dart 语言 HelloWorld 示例中的 Client 端的请求处理拿过来后,简单修改了下。

    import 'package:grpc/grpc.dart'; import 'src/generated/helloworld.pbgrpc.dart'; 复制代码

    class _MyHomePageState extends State<MyHomePage> {   String _message = '';   void _callGRPC() async {     final channel = ClientChannel(       'localhost',       port: 50051,       options: ChannelOptions(         credentials: ChannelCredentials.insecure(),         codecRegistry:             CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),       ),     );     final stub = GreeterClient(channel);     try {       final response = await stub.sayHello(         HelloRequest()..name = 'flutter world',         // options: CallOptions(compression: const GzipCodec()),       );       setState(() {         _message = response.message;       });       print('Greeter client received: ${response.message}');     } catch (e) {       print('Caught error: $e');     }     await channel.shutdown();   }   @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: Text(widget.title),       ),       body: Center(         child: Column(           mainAxisAlignment: MainAxisAlignment.center,           children: <Widget>[             const Text(               'You have pushed the button this many times:',             ),             Text(               _message,               style: Theme.of(context).textTheme.headline4,             ),           ],         ),       ),       floatingActionButton: FloatingActionButton(         onPressed: _callGRPC,         tooltip: 'call gRPC',         child: const Icon(Icons.add),       ),      );   } } 复制代码

    flutter run运行下试试。
    需要先启动上面的 go 语言的 Server 端

    Launching lib/main.dart on macOS in debug mode... lib/src/generated/helloworld.pbgrpc.dart:5:1: Error: A library can't opt out of null safety by default, when using sound null safety. // @dart = 2.7 ^^^^^^^^^^^^^^ lib/src/generated/helloworld.pb.dart:5:1: Error: A library can't opt out of null safety by default, when using sound null safety. // @dart = 2.7 ^^^^^^^^^^^^^^ Command PhaseScriptExecution failed with a nonzero exit code note: Using new build system note: Building targets in parallel note: Planning build note: Constructing build description ** BUILD FAILED ** Building macOS application...                                            Exception: Build process failed 复制代码

    报了一堆空安全的错误。

    原因是gRPC官方的 dart 示例用的 库还比较旧,还没加入空安全。所以从 proto 文件生成的 dart 文件不支持空安全。

    那先忽略空安全吧。

    最好的方式,应该是用最新的支持dart空安全的 gRPC 库,基于 proto 文件重新生成通信用的 dart 文件。
    不过还没确认最新的库支持不支持空安全。

    flutter run --no-sound-null-safety运行下。
    需要先启动上面的 go 语言的 Server 端

    What? 又报错了。

    flutter: Caught error: gRPC Error (code: 14, codeName: UNAVAILABLE, message: Error connecting: SocketException: Connection failed (OS Error: Operation not permitted, errno = 1), address = localhost, port = 50051, details: null, rawResponse: null, trailers: {}) 复制代码

    其实错误的原因很简单,就是没有权限。但是由于经验不足,这个问题困扰了我很久。后来一个 Flutter 群里的兄台和我说需要加上权限。

    就是下面这个文章里的内容:
    Flutter gRPC 开发桌面应用 code: 14 / codeName: UNAVAILABLE / Operation not permitted - 掘金 (juejin.cn)

    在 macos/Runner 的 DebugProfile.entitlements 和 Release.entitlements 里加上权限。

    <key>com.apple.security.network.client</key> <true/> 复制代码

    DebugProfile.entitlements:

    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict>         <key>com.apple.security.app-sandbox</key>         <true/>         <key>com.apple.security.cs.allow-jit</key>         <true/>         <key>com.apple.security.network.server</key>         <true/>         <key>com.apple.security.network.client</key>     <true/> </dict> </plist> 复制代码

    Release.entitlements:

    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict>         <key>com.apple.security.app-sandbox</key>         <true/>         <key>com.apple.security.network.client</key>     <true/> </dict> </plist> 复制代码

    flutter run --no-sound-null-safety再次运行下。
    需要先启动上面的 go 语言的 Server 端

    点击右下的 FAB,显示了来自 go 语言 Server 端的消息。
    大功告成!!!

    image.png

补充

上面的 flutter 工程在打包时也需要加上 --no-sound-null-safety

例:
Mac 下的打包命令:

flutter build macos --no-sound-null-safety 复制代码

后记

  1. 上面有些处理,是折中的临时方案,而且是基于 gRPC 官方的示例来做的。
    正式的开发中,应该是下面这样。

    • 先做成 proto 文件。

    • 分别基于相同的 proto 文件生成 Server 端的代码和 Client 端的代码。

    • 编写 Server 端处理 和 Client 处理。

    • 然后启动 Server 端,再运行 Client 端。

  2. 本来是个很简单的技术点,中间走了一些弯路。
    已经有很多人这么做了,只是感觉网上相关文章不多,自己简单写一下,也是记录一下技术点。

  3. 上面的 flutter 工程 Mac 下打包出来的执行文件大小为38.4M。
    go 语言 Server 端打包出来的执行文件大小为 11.1M。
    加起来感觉也不小呢~试了个寂寞?
    go 语言 Server 端一个运行时,dart 语言 Client 端一个运行时,加起来比 Electron 的运行时还是能小点。


作者:bettersun
链接:https://juejin.cn/post/7022173043791233031


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐