这篇文章极其具有时效性,在目前,flutter 不支持多窗口,但是已经有这么一篇文档[1]。而且 flutter desktop 的多窗口可能已经计划中了。

SETSUNA 的 UI 需要满足条件:桌面移动端统一 UI(没错过了半年还在选型)。现阶段可以选择的只有一个框架:flutter。而且:

  • flutter 使用 skia 绘图而不是调用原生组件,表现统一
  • flutter 可以方便的响应式设计,UI 可以用同一套代码(只要可以,顺路兼容手机、折叠屏、横向平板、桌面布局)

flutter 本就是先给移动端设计,可以直接用 dart 完成很多工作。但是在调研桌面时发现一个问题:找不到多窗口 API! 这不行。虽然 flutter 是允许原生编程,那我能会 windows 开发吗?!

等会,flutter 是不是已经支持了 web。那么,这波不得两面包夹芝士?

flutter-tauri

把 flutter 套进 electron 是不是就完成需求了!(顺便还可以实现简单 PWA 版本)

但是桌面端不选择 electron,咱用 tauri!毕竟现在全平台都 webkit(blink),而且 flutter 使用 canvaskit,不需要担心表现…

准备工作

初始化

tauri 采用的是「集成到项目」的方式,所以先创建一个 flutter 项目(创建步骤省略)

接着,根据文档[2]集成 tauri:

1
2
$ yarn add -D @tauri-apps/cli
$ yarn tauri init

需要配置一些步骤。其中,distDir 选择的是 flutter 创建的 web/。开发地址暂时空过

flutter 以服务运行

与 electron 一样,tarui 运行的还是前端应用,所以要不执行 index.html,要不有个开发服务

flutter 直接调试会直接打开 chrome,所以这次运行换种方式:仅运行开发服务

1
2
# 如果不指定端口,flutter 会随机一个高位端口,但每次启动都会更换端口
flutter run -d web-server --web-port 4396

flutter 服务没有热更,要更新的时候按 R 换弹 更新

配置 tauri 启动

还记得刚刚漏了的运行地址?tauri 的配置都在 tauri.conf.json 中,找到 devPath 并补上

接着直接运行

1
yarn tauri dev

顺利的话就能直接看到窗口了

flutter-tauri

开整

虽然标题只提到了「多窗口」,但 realworld 肯定无法避开原生操作。而且不管是 tauri 还是 electron 都需要用 JavaScript

tauri 前后端(rust)通讯采用命令(command)的方式,同样是通过 event bus 互相抛事件来做到。所以我们要让一切 JavaScript 的工作桥接到 dart 上

tauri 提供了两种向后端通信的方式:直接用挂载在 window 的对象或者使用提供的包:tauri-app。这里暂时先用 window,但后者也可以满足,后面写 dartjs 互操作篇再覆盖

接下来分成两部分:需要实例化调用 与 不需要实例化调用

不需要实例化

其实意思就是可以直接访问直接调用,不用 new。而在 tauri 中,向后端发起命令的函数位于:window.__TAURI__.invoke

按照我们使用 JavaScript 的思路,调用一个对象下面的方法,思路就是直接一直点(.)下去。那么这就是一个访问多层上下文的过程

可以了,换成 dart 写出来

1
2
3
4
5
6
7
// flutter 肯定有 dart,那肯定有 dart:js
import 'dart:js' as js;

// 获得 tauri 挂载的对象
final tauri = js.context['__TAURI__'];
// 调用这个对象里面的方法
tauri.callMethod('invoke', ['']);

需要实例化

总有(一堆)东西需要 new,多窗口就是其中一个。(这就离谱,多窗口竟然不由 rust 发起创建)

负责创建窗口的 API 位于:window.__TAURI__.window.WebviewWindow
(更 NM 离谱的是直接明牌告诉你再加一个 webview)

那么,JavaScript 需要实例化,意味着需要在 dart 也实现一遍实例化作为映射。等于我们要在 dart 有一个对应的 class,有 class 才能 new

dart 里边儿能有这玩意儿吗!那妹有怎么办啊?造啊!

这次不能用dart:js,得换一个

1
flutter pub add js

然后是模拟一切。首先最好先创建一个新文件,这个文件会被声明为某个可以被 JavaScript “发现”的 library

1
2
3
4
@JS()
library tauri; // 实际上在这篇文章这块没卵用

import 'package:js/js.dart';

接着,这波我们要实例化的是 WebviewWindow,位于 window.__TAURI__.window.WebviewWindow

1
2
@JS('__TAURI__.window.WebviewWindow')
class WebviewWindow {}

那 class 要能 new,得有一个 constructor,dart 和某些其他语言一样,再写一遍类名。其他方法该怎么写怎么写

再看一眼 tauri 文档,WebviewWindow 实例化带两个参:labeloptionsoptions是一个对象,关于新窗口的配置

1
2
3
4
@JS('__TAURI__.window.WebviewWindow')
class WebviewWindow {
external WebviewWindow(String id, Object options);
}

再看一眼文档,options 的 type 是 WindowOptions,所以这里可以如法炮制,再造出一个结构

1
2
3
4
5
6
7
@anonymous
@JS()
class WindowOption {
external factory WindowOption({
String title, // 文章例子仅需要 title
});
}

这样写的结构,仅支持 getter,所以如果需要 setter,可以按照 dart 的方式改写这个 class

接着把上面的改一下。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// tauri.dart
@JS()
library tauri;

import 'package:js/js.dart';

@JS('__TAURI__.window.WebviewWindow')
class WebviewWindow {
external WebviewWindow(String id, WindowOption options);
}

@anonymous
@JS()
class WindowOption {
external factory WindowOption({
String title,
});
}

main.dart创建一个新的按钮用来测试

1
2
3
4
5
6
7
8
9
ElevatedButton(
onPressed: () {
final window = WebviewWindow(
"new_window",
WindowOption(title: "Second Window"),
);
},
child: Text('click'),
)

完结撒花,结果不给图


  1. Desktop Multi-Window Support (PUBLICLY SHARED) ↩︎

  2. Tauri Integration ↩︎