主题
虽然 Flet 控件 借助众多内置的 Flutter widgets 来实现复杂应用的构建,但并非所有 Flutter 控件或第三方包都能由 Flet 团队直接支持,或被纳入 Flet 核心框架。与此同时,Flutter 生态十分庞大,为开发者提供了远超核心功能的广泛扩展可能。
为此,Flet 框架提供了一套扩展机制。它让你能够把自己自定义 Flutter 包或第三方库中的控件和 API 直接引入到你的 Flet 应用里。
在本指南中,你将学习如何从模板创建 Flet 扩展,然后把它定制为把第三方 Flutter 包集成到你的 Flet 应用中。
前置条件
要把自定义 Flutter 包集成进 Flet,你需要基本了解如何用 Dart 语言创建 Flutter 应用和包,并配置好 Flutter 开发环境。关于 Flutter 和 Dart 的更多信息,参见 Flutter 入门。
从模板创建 Flet 扩展
Flet 现在让你可以轻松创建并构建基于 Flutter 控件或 Flutter 第三方包、带有自定义控件的项目。在下面的示例中,我们将基于 flutter_spinkit 包创建一个自定义的 Flet 扩展。
第 1 步。 创建新的虚拟环境,并在其中安装 Flet。
第 2 步。 从模板创建新的扩展项目。
flet create --template extension --project-name flet-spinkit会创建一个带有新 FletSpinkit 控件的项目。这个控件目前只是一个带 value 属性的 Flutter Text 控件,我们稍后会定制它。
第 3 步。 构建示例应用。
从扩展模板创建的 Flet 项目带有 examples/flet_spinkit_example 文件夹,里面是示例应用。
在应用的 pyproject.toml 所在目录(examples/flet_spinkit_example)中,运行 flet build 命令,例如针对 macOS:
flet build macos -v打开应用,查看新的自定义 Flet 控件:
open build/macos/flet-spinkit-example.app
修改 Python 文件
一旦项目为桌面端构建过一次,你就可以修改 Python 文件并直接运行,而无需重新构建。
首先,如果你没在用 uv,从 pyproject.toml 安装依赖:
pip install .或
poetry install现在你可以修改示例应用的 main.py:
python
import flet as ft
from flet_spinkit import FletSpinkit
def main(page: ft.Page):
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(
ft.Container(
height=150,
width=300,
alignment=ft.Alignment.CENTER,
bgcolor=ft.Colors.PINK_200,
content=FletSpinkit(
tooltip="My new PINK FletSpinkit Control tooltip",
value="My new PINK FletSpinkit Flet Control",
),
),
)
ft.run(main)然后运行:
flet run
修改 Flutter 包
当你对 flutter 包做任何改动时,都需要重新构建:
flet build macos -v如果需要调试,运行这个命令:
build/macos/flet-spinkit-example.app/Contents/MacOS/flet-spinkit-example --debug集成第三方 Flutter 包
我们把 flutter_spinkit 包集成进我们的 Flet 应用。
第 1 步。 添加依赖
进入 src/flutter/flet_spinkit 文件夹,运行以下命令把对 flutter_spinkit 的依赖加入 pubspec.yaml:
flutter pub add flutter_spinkit关于使用 Flutter 包的更多信息,请阅读此处。
第 2 步。 修改 dart 文件
在 src/flutter/flet_spinkit/lib/src/flet_spinkit.dart 文件中,添加 import 语句,并把 Text 控件替换为 SpinKitRotatingCircle 控件:
dart
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class FletSpinkitControl extends StatelessWidget {
final Control control;
const FletSpinkitControl({
super.key,
required this.control,
});
@override
Widget build(BuildContext context) {
Widget myControl = SpinKitRotatingCircle(color: Colors.red, size: 100.0);
return LayoutControl(control: control, child: myControl);
}
}第 3 步。 重新构建示例应用
进入 examples/flet_spinkit_example,用 flet clean 删除上一次构建产物,然后重新构建你的应用:
flet build macos -v第 4 步。 运行你的应用

Flet 扩展结构
用扩展模板创建新的 Flet 项目后,你会看到如下文件夹结构:
├── LICENSE
├── mkdocs.yml
├── README.md
├── docs
│ └── index.md
│ └── FletSpinkit.md
├── examples
│ └── flet_spinkit_example
│ ├── README.md
│ ├── pyproject.toml
│ └── src
│ └── main.py
├── pyproject.toml
└── src
├── flet_spinkit
│ ├── __init__.py
│ └── flet_spinkit.py
└── flutter
└── flet_spinkit
├── CHANGELOG.md
├── LICENSE
├── README.md
├── lib
│ ├── flet_spinkit.dart
│ └── src
│ ├── create_control.dart
│ └── flet_spinkit.dart
└── pubspec.yamlFlet 扩展由以下部分组成:
- 包,位于
src文件夹 - 示例应用,位于
examples/flet_spinkit_example文件夹 - 文档,位于
docs文件夹
包
包是将在你的应用中使用的组件。它由两部分组成:Python 和 Flutter。
Python
flet_spinkit.py
定义 Python 侧的 Flet 控件。FletSpinkit 通过 @ft.control("FletSpinkit") 注册,并继承自 ft.LayoutControl,这把它与扩展中处理的 Flutter Control.type 绑定起来。该类目前包含一个 value: str 属性和一个占位用的文档字符串。
Flutter
pubspec.yaml
扩展的 Flutter 包清单。声明 SDK 约束和依赖。值得注意的依赖:
flet提供 Flet 扩展 APIflutter_spinkit提供该控件使用的旋转控件
flet_spinkit.dart
库的入口点。从 extension.dart 导出公开的 Extension 类。
src/extension.dart
用 Flet 注册扩展。Extension.createWidget 把 Control.type 映射到 Flutter 控件;目前把 "FletSpinkit" 映射到 FletSpinkitControl。
src/flet_spinkit.dart
控件的 Flutter 包装控件。FletSpinkitControl 构建一个 SpinKitRotatingCircle,并用 LayoutControl 包裹它,以便应用来自 Python 控件的布局/状态。
示例应用
src/main.py
使用 Flet Python 控件的 Python 程序。
pyproject.toml
在这里你指定对包(即你的扩展)的依赖,它可以是:
- 路径依赖
指向你 Flet 扩展文件夹的绝对路径,例如:
dependencies = [
"flet-spinkit @ file:///Users/user-name/projects/flet-spinkit",
"flet>=0.80.2",
]- Git 依赖
指向 git 仓库的链接,例如:
dependencies = [
"flet-ads @ git+https://github.com/flet-dev/flet-ads.git",
"flet>=0.80.2",
]- PyPi 依赖
发布在 pypi.org 上的包名,例如:
dependencies = [
"flet-ads",
"flet>=0.80.2",
]文档
如果你打算把扩展分享给社区,可以轻松地从源码用 mkdocs 生成文档。
Flet 扩展自带一个 docs 文件夹,里面包含文档的初始文件,以及项目根目录下的 mkdocs.yml 文件。
在包含 mkdocs.yml 的文件夹中,运行以下命令,在本地查看文档长什么样:
mkdocs serve在浏览器中打开 http://127.0.0.1:8000:

文档准备好后,如果你的包托管在 GitHub 上,可以运行以下命令把文档托管到 GitHub Pages 上:
mkdocs gh-deploy你可能会发现这份指南 对入门 mkdocs 有帮助。
定制属性
在上面的示例中,Spinkit 控件创建了一个硬编码的 Flutter 控件。现在我们来定制它的属性。
通用属性
总的来说,Flet 中有三类控件:
添加到应用/页面表面的视觉控件,例如
FletSpinkit。从页面打开的对话框及其他弹窗控件(对话框、选择器、面板等)(例如
page.show_dialog(dlg))。服务(Clipboard、Battery、Audio 等),它们作为独立实例创建,并自动注册到页面。
当你创建一个应该参与布局(尺寸、位置、变换、边距等)的视觉控件时,定义一个用 @ft.control("control_name") 注解的、基于 dataclass 的控件,并继承自 LayoutControl。在其 Dart 对应部分(src/flet_spinkit.dart)中,用 LayoutControl(...) 包裹你的 Flutter 控件。
当你创建一个对话框或其他弹窗控件(用 page.show_dialog(...) 打开)时,定义一个用 @ft.control("control_name") 注解的、基于 dataclass 的控件,并继承自 DialogControl。在其 Dart 对应部分,显示对话框/弹窗(例如 showDialog 或 showModalBottomSheet),并返回一个像 SizedBox.shrink() 这样的占位控件,而不是用 LayoutControl(...) 或 BaseControl(...) 包裹。
当你创建一个服务控件(Clipboard、Battery、Audio 等)时,定义一个用 @ft.control("control_name") 注解的、基于 dataclass 的控件,并继承自 Service。在其 Dart 对应部分,实现 FletService,并通过 FletExtension.createService 注册它(无需控件包裹)。
你可以直接使用基于 dataclass 的控件继承来的所有 LayoutControl、DialogControl 和 Service 属性,而无需把它们重新声明为字段(除非你想覆盖默认值或元数据)。
如果你是从 Flet 扩展模板创建的扩展项目,你的 Python 控件已经继承自 LayoutControl,你可以在示例应用中使用它的属性:
python
import flet as ft
from flet_spinkit import FletSpinkit
def main(page: ft.Page):
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(
ft.Stack(
[
ft.Container(height=200, width=200, bgcolor=ft.Colors.BLUE_100),
FletSpinkit(opacity=0.5, tooltip="Spinkit tooltip", top=0, left=0),
]
)
)
ft.run(main)
控件专属属性
既然你已经充分利用了 Flet LayoutControl 提供的属性,现在来定义专属于你正在构建的新控件的属性。
在 FletSpinkit 示例中,我们来定义它的 color 和 size。
在 Python 类中,定义新的 color 和 size 属性:
python
from typing import Optional
import flet as ft
@ft.control("flet_spinkit")
class FletSpinkit(ft.LayoutControl):
"""
FletSpinkit Control description.
"""
color: Optional[ft.ColorValue] = None
size: float = 100.00在 src/flet_spinkit.dart 文件中,使用辅助方法 getColor 和 getDouble 来获取 color 和 size 的值:
dart
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class FletSpinkitControl extends StatelessWidget {
final Control control;
const FletSpinkitControl({
super.key,
required this.control,
});
@override
Widget build(BuildContext context) {
Widget myControl = SpinKitRotatingCircle(
color: control.getColor("color", context),
size: control.getDouble("size") ?? 100.0,
);
return LayoutControl(control: control, child: myControl);
}
}在你的应用中使用 color 和 size 属性:
python
import flet as ft
from flet_spinkit import FletSpinkit
def main(page: ft.Page):
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(
ft.Stack(
controls=[
ft.Container(height=200, width=200, bgcolor=ft.Colors.BLUE_100),
FletSpinkit(
color=ft.Colors.YELLOW,
size=150,
opacity=0.5,
tooltip="Spinkit tooltip",
top=0,
left=0,
),
]
)
)
ft.run(main)重新构建并运行:

重要:默认值在两侧必须一致
在 Python 侧带有默认值的属性,当用户没有从默认值改动它时,不会发送到 Flutter。这意味着你的 Dart 代码必须为每一个在 Python 中有默认值的属性提供相同的默认值。
例如,如果你的 Python 控件声明:
python
size: float = 100.0
animate: bool = True那么你的 Dart 代码必须使用匹配的默认值:
dart
// 正确 - 默认值与 Python 匹配
final size = control.getDouble("size", 100.0)!;
final animate = control.getBool("animate", true)!;
// 错误 - 没有默认值,当用户未设置该属性时为 null
final size = control.getDouble("size"); // 返回 null!
final animate = control.getBool("animate"); // 返回 null!这也适用于用辅助函数解析的 @ft.value 类型。如果一个值类型的字段有默认值,Dart 侧对应的 parseDouble()、parseBool()、parseDuration() 等调用必须提供相同的默认值。
常见陷阱:
- 缺少默认值:
control.getDouble("prop")而不是control.getDouble("prop", 0.0)! - 默认值不匹配:Python 是
True,但 Dart 默认是false - 单位不匹配:Python 用
Duration(milliseconds=150),但 Dart 用Duration(microseconds: 150) - 空集合:
field(default_factory=list)意味着空列表不会被发送;Dart 必须用?? const []处理 null
可以在此处找到本示例的源码。
专用数据通道
Python 和 Dart 之间的属性更新和方法调用走的是 Flet 基于 MsgPack 帧的控件协议。这对控件状态和小事件挺好,但在每帧多次分配的情况下上限约为 1 GB/s。对于需要传输批量二进制数据的控件——图像帧、音频缓冲、文件块、ML 推理张量——Flet 提供了专用的 DataChannel,它完全绕过 MsgPack,在每个受支持平台上都能达到内存带宽级别的吞吐量。
受益于数据通道的控件示例:
- 一个从 Python 接收位图流的图表(1080p RGBA、60 fps ≈ 480 MB/s——远超控件协议能维持的速率)
- 一个把帧从 Dart 推送到 Python 做 ML 推理的摄像头控件
- 一个把 PCM 样本流式传输到 Python DSP 流水线的麦克风控件
对于 KB 以下的控件状态,你不需要这个——在那个量级下常规属性协议端到端更快。当载荷从几 KB 起步、尤其是高速流式传输时,才用 DataChannel。
它如何工作
分配发生在 Dart 侧:控件在 initState 中调用 FletBackend.of(context).openDataChannel(),然后触发一个名为 data_channel_open 的控件事件,携带 {channel_name, channel_id}。Python 侧声明一个 on_data_channel_open 处理器,通过 Control.get_data_channel(channel_id) 取到匹配的通道,并开始发送/接收字节。
传输按部署模式自动选择——在嵌入式原生模式(flet build 针对桌面/移动端)下每个通道有专属的 PythonBridge,在 Pyodide 下通过 postMessage 配合 Transferable ArrayBuffer 以原始字节帧复用,在 flet run 开发模式下通过协议套接字复用,在配合 Python 服务器的场景下通过 WebSocket 复用。控件代码在所有模式下都完全一致。
Python 侧 API
python
from typing import Callable, Optional
import flet as ft
@ft.control("MyImageChart")
class MyImageChart(ft.LayoutControl):
on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]] = None
def init(self) -> None:
# `init` 是 @ft.control 的构造后钩子(在 `did_mount` 之前运行)。
# 在这里接上捕获通道的处理器。
self._frames: Optional[ft.DataChannel] = None
if self.on_data_channel_open is None:
self.on_data_channel_open = self._capture_channel
def _capture_channel(self, e: ft.DataChannelOpenEvent) -> None:
# `e.channel_name` 是 Dart 侧放进事件负载中的标签
# ——当一个控件打开多个通道时,用它来区分分发。
# 单通道控件可以忽略它。
self._frames = self.get_data_channel(e.channel_id)
# 可选:订阅从 Dart → Python 流动的字节。
self._frames.on_bytes(self._on_frame_from_dart)
def push_frame(self, rgba_bytes: bytes) -> None:
"""Python → Dart —— 发出即弃的字节发送。"""
if self._frames is not None:
self._frames.send(rgba_bytes)
def _on_frame_from_dart(self, payload: bytes) -> None:
# 从传输的投递线程(在嵌入式原生模式下位于 GIL 之下)
# 调用。推到一个 `queue.Queue` 并让工作线程去排空
# ——不要在这里做重 CPU 工作,否则会拖垮传输。
...ft.DataChannel 是一个抽象类——实例来自 Control.get_data_channel(channel_id)。它的接口:
send(payload: bytes)— Python → Dart,发出即弃on_bytes(callback: Callable[[bytes], None] | None)— 注册一个处理 Dart 推送字节的处理器;传None可清除close()— 释放通道(幂等)。控件卸载时它不会自动关闭,因此请在控件的will_unmount()中关闭它
ft.DataChannelOpenEvent 的字段是 channel_name: str 和 channel_id: int。字段名是 channel_name 而不是 name,因为 name 在基类的 Event 上被保留用作事件自身的名字("data_channel_open")。
Dart 侧 API
dart
import 'package:flet/flet.dart';
class MyImageChartState extends State<MyImageChartWidget> {
late final DataChannel _frames;
StreamSubscription<Uint8List>? _sub;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_sub != null) return; // 惰性初始化,只做一次
_frames = FletBackend.of(context).openDataChannel();
_sub = _frames.messages.listen(_onFrameFromPython);
// 通过常规控件事件把通道告知 Python。
widget.control.triggerEvent("data_channel_open", {
"channel_name": "frames",
"channel_id": _frames.id,
});
}
void _onFrameFromPython(Uint8List bytes) {
// 交给 Texture、dart:ui.Image.fromPixels 等处理。
}
void _pushFrameToPython(Uint8List bytes) {
_frames.send(bytes);
}
@override
void dispose() {
_sub?.cancel();
_frames.close();
super.dispose();
}
}DataChannel 的 Dart 侧接口:
int get id— 进入data_channel_open事件负载的通道标识符Stream<Uint8List> get messages— 从 Python 推送来的字节bool send(Uint8List bytes)— Dart → Python,发出即弃void close()— 释放通道(幂等)
FletBackend.of(context) 不会让控件订阅变化,因此通道既可以在 initState 中分配,也可以像上面那样在 didChangeDependencies 中(加一次只运行一次的守卫)分配。
两侧都不需要导入 serious_python 或 dart_bridge。DataChannel 的 API 接口完全位于 package:flet / flet 中,所以你的扩展依赖保持不变。
每个控件多个通道
一个控件可以按需打开任意多个通道——每次 openDataChannel() 调用都会生成一个唯一 id。用事件中的 channel_name 来区分它们:
dart
// Dart —— 打开两个通道,分别打标签
_frames = FletBackend.of(context).openDataChannel();
_audio = FletBackend.of(context).openDataChannel();
widget.control.triggerEvent("data_channel_open", {
"channel_name": "frames",
"channel_id": _frames.id,
});
widget.control.triggerEvent("data_channel_open", {
"channel_name": "audio",
"channel_id": _audio.id,
});python
# Python —— 按 channel_name 分发
def _on_data_channel_open(self, e: ft.DataChannelOpenEvent) -> None:
match e.channel_name:
case "frames":
self._frames = self.get_data_channel(e.channel_id)
case "audio":
self._audio = self.get_data_channel(e.channel_id)线程与背压
on_bytes 处理器在 GIL 之下同步运行,运行在传输所投递自的那个操作系统线程上。对于任何重 CPU 的工作(PNG 解码、ML 推理),把负载推到一个 queue.Queue 或 asyncio.Queue 并让工作线程去排空——阻塞投递线程会拖垮传输。
接收侧的队列默认无界。如果生产者快于消费者(摄像头帧进入慢解码器、matplotlib 旋转进入 Flutter 绘制),内存就会增长。对于媒体流式控件,在生产者侧实现背压或丢弃旧数据的策略。一个参考示例是 MatplotlibChartCanvas——它用来自 Dart 的 1 字节 ack 帧,让生产者逐帧等待,对应 matplotlib WebAgg 的 waiting 标志模式。
另见
- 第一方参考控件:
MatplotlibChartCanvas—— 从_invoke_methodPNG 分发完整迁移到 1 字节操作码数据通道,包含背压 ack 以及 GPU / CPU 渲染策略。Python · Dart - 完整设计 / 性能说明(线格式、跨模式运行、并发模型、实测数字):
flet-dev/serious-python中的dedicated-data-channels.md。 - 线格式协议升级,供实现自定义 Flet 后端或 sidecar 的人参考:Flet 协议帧格式升级以支持 DataChannel。
启动画面
扩展可以提供一个自定义的启动画面——也就是打包后的应用在首个页面出现之前、启动过程中显示的画面(关于终端用户/配置侧,请见发布指南中的启动画面)。这让你能完全自定义启动体验,包括自定义布局和动画,而不是使用内置的 flet 画面。
createBootScreen 钩子
在你的 FletExtension 上(位于 src/extension.dart)覆盖 createBootScreen。一个扩展可以提供多个具名画面:
dart
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
import 'my_boot_screen.dart';
class Extension extends FletExtension {
@override
Widget? createBootScreen(String name, Map<String, dynamic> options,
ValueListenable<BootStatus> status) {
switch (name) {
case "my_screen":
return MyBootScreen(options: options, status: status);
default:
return null; // 未处理 —— Flet 尝试下一个扩展,然后是内置的 "flet" 画面
}
}
}这个钩子接收:
name— 配置的画面名(来自[tool.flet.boot_screen].name)。 对你不处理的名字返回null。options— 来自pyproject.toml的[tool.flet.boot_screen.<name>]表,作为Map<String, dynamic>。把你的画面所需的任意选项放在那里。status— 一个ValueListenable<BootStatus>,报告当前启动 状态。用ValueListenableBuilder监听它以实时更新 UI。
BootStatus 携带:
| 字段 | 类型 | 描述 |
|---|---|---|
stage | BootStage | BootStage.preparing(解包应用包体——仅 Android)或 BootStage.startingUp(Python 运行时 + 应用启动中)。 |
error | String? | 启动失败时非空;该消息已可直接显示。 |
示例控件
dart
import 'package:flet/flet.dart';
import 'package:flutter/material.dart';
class MyBootScreen extends StatelessWidget {
final Map<String, dynamic> options;
final ValueListenable<BootStatus> status;
const MyBootScreen({super.key, required this.options, required this.status});
@override
Widget build(BuildContext context) {
// 从 pyproject.toml 读取选项。颜色接受十六进制(#rrggbb)或
// 通过 Flet 的 HexColor 辅助函数使用的具名颜色。
final bgcolor =
HexColor.fromString(null, options["bgcolor"] as String?) ?? Colors.black;
return ValueListenableBuilder<BootStatus>(
valueListenable: status,
builder: (context, boot, _) {
if (boot.error != null) {
return Scaffold(
backgroundColor: bgcolor,
body: Center(
child: Text(boot.error!,
style: const TextStyle(color: Colors.redAccent)),
),
);
}
final message = boot.stage == BootStage.preparing
? "Preparing…"
: "Starting up…";
return Scaffold(
backgroundColor: bgcolor,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(message, style: const TextStyle(color: Colors.white)),
],
),
),
);
},
);
}
}然后在应用的 pyproject.toml 中按名字选择你的画面:
toml
[tool.flet.boot_screen]
name = "my_screen"
[tool.flet.boot_screen.my_screen]
bgcolor = "#101020"INFO
在打包后的应用中,启动画面由一个持久的宿主覆盖层渲染,所以你的控件不会在准备和启动两个阶段之间重新挂载——同一个实例(及其动画)会被复用,宿主在应用就绪时把它淡出。让你的 UI 由 status 驱动,并且在准备阶段不要依赖应用的 Theme 来取颜色——从 options 中读取,这样画面在整个过程中看起来才一致。宿主还会从你的 options 中读取 fade_out_duration(毫秒)来控制这个淡出。
真实示例
flet-spinkit 扩展就附带了一个完全以此方式构建的 spinkit 启动画面——参见 boot_screen.dart 和它的 extension.dart。
示例
Flet 拥有一些以内置扩展形式实现的控件,可以作为你自己控件的起点。