带个界面就得上 React、Vue,不会前端连个页面都做不了?我以前也是这么想的,直到我遇见了 Flet。这个基于 Flutter 的 Python 框架,让我一个纯后端出身、写 Python 爬虫和脚本的人,也能轻松搞出能跑在 Android、iOS、Web 甚至桌面端的跨平台应用。今天这篇就从头到尾聊聊,我是怎么用 Flet 从零写了一个能打包成 APK 的 App,顺便把中间踩过的坑、查过的资料、试过的方案都整理出来,给想走这条路的朋友一个参考。
这篇文章不假设你有任何前端基础,但默认你至少熟悉 Python 的基本语法——比如会写个函数、知道pip装包是怎么回事。我会尽量把每个环节讲透,尤其是网上资料比较少的那部分:打包 APK、处理 Gradle 报错、多线程更新 UI。如果你正在纠结“用 Python 能不能做 App”,这篇文章应该能给你一个比较明确的答案。
1. 核心思路与方案选型:为什么偏偏是 Flet
1.1 先弄明白 Flet 到底是什么
Flet 在官方的定位是 “Flutter for Python”,它把 Flutter 的 UI 模型映射成了 Python 的控件模型,让你用纯 Python 语法描述界面,跑起来之后实际上是调用 Flutter 的渲染引擎来绘制。这里有个关键点要理解清楚:Flet 不是把 Python 代码翻译成 Java/Kotlin/Swift 原生代码,而是让 Python 代码通过 WebSocket 实时和 Flutter 引擎通信,把控件树、交互事件、数据更新同步过去。
打个比方,Flet 就像一个远程遥控器,Python 代码是遥控器面板,Flutter 引擎是电视屏幕,你按遥控器的时候,电视就跟着变画面。这个架构最直接的好处是,你不直接写 Dart,也不用碰 XML 布局文件,所有 UI 逻辑全部留在 Python 里,这对 Python 开发者来说几乎没有任何额外学习成本。
我最早接触到这个项目是在 GitHub 上看到一个简单示例,十几行代码就启动了一个可交互的页面。当时我就在想,这不就是我一直想要的东西吗?后来翻文档才发现它背后是 Flutter,顿时就明白为什么UI渲染效果那么流畅了。
1.2 和 Electron、Kivy、Tauri 比,优势在哪
在 Flet 出现之前,Python 开发桌面应用最常用的方案是 Tkinter 和 PyQt,后来有 Electron 配 Python 后端,也有 Kivy 这种主打移动端的跨平台框架。每个方案我都或多或少折腾过,说说我的体感:
- Electron + Python 后端:最难搞的是打包分发给用户,Electron 应用体积基本都是 100MB 起步,而且内存占用长期居高不下。我做过一个简单的工具型 App,打开后内存直接到 300MB,用户吐槽不断。
- Tkinter:写起来倒是简单,就是界面风格实在太过时了,二十一世纪的设计语言它一个都不占,现代一点的交互效果基本做不出来。
- PyQt / PySide:功能强大,UI 写起来也算顺手,但一是授权协议有讲究,二是界面和交互往往需要靠 QSS 手动打磨,想做得漂亮挺费功夫。
- Kivy:官方主打移动端,但环境配置相对复杂,在 Android 上的打包流程也比较繁琐,社区资料和示例偏少,遇到问题解决问题的时间成本很高。
Flet 的出现对我来说,恰好把这个组合问题给解掉了:界面上它继承了 Flutter 的 Material Design 风格,视觉效果有保障;开发语言是纯 Python,对后端和脚本出身的开发者非常友好;打包则直接借助 Flutter 的构建管线,一套代码目标覆盖 Android、iOS、Web 和桌面端。可以说,Flet 把“像写 Python 脚本一样写应用界面”和“像 Flutter 一样高性能渲染”这两件事揉在了一起。
2. 环境准备与快速上手:先把第一个界面跑起来
2.1 Python 环境安装的几个坑
既然要用 Flet,Python 环境是第一步。关于 Python 的安装,网上教程很多,但我还是踩过几个坑,尤其针对 Windows 用户,有几点值得特别提一提:
下载 Python 时,安装向导第一屏的Add Python to PATH一定要勾选。很多朋友第一次装完 Python,在命令行敲
python会看到 “Python was not found; run without arguments to install from the Microsoft Store” 这种提示,八成就是没勾这个选项,或者装了 Microsoft Store 版本的 Python。装完在命令行验证版本时你会发现,
python和python3可能不一样。Windows 下通常用python,macOS/Linux 下python3更常用。为避免混乱,我建议统一用python -V验证自己当前的默认版本。一定要用虚拟环境。我见过太多人在系统全局环境里装了一堆包,最后不同项目之间互相打架。Flet 项目建议单独建一个虚拟环境,比如:
python -m venv flet_envWindows 下激活是flet_env\Scripts\activate,macOS/Linux 下是source flet_env/bin/activate。激活后命令行前面会出现(flet_env)的提示,这时候装的包都隔离在这个环境里,不会污染全局。
2.2 安装 Flet 并启动第一个页面
装 Flet 很简单,激活虚拟环境后:
pip install flet然后检查安装版本:
flet --version如果能看到版本号,就说明装好了。接着写一个最简单的页面,验证一下整套流程:
import flet as ft def main(page: ft.Page): page.title = "我的第一个 Flet 应用" page.add( ft.Text("Hello, Flet!") ) ft.app(target=main)保存文件为hello.py,然后在命令行运行:
python hello.py这时候会自动弹出一个桌面窗口,显示 “Hello, Flet!”。如果你的电脑没有桌面环境(比如纯净服务器),Flet 也支持只启动 Web 模式:
flet run --web hello.py这种方式会在本机起一个 HTTP 服务,你用浏览器打开http://localhost:8550就能看到界面了。我觉得这个特性特别适合在开发调试阶段快速预览。
2.3 用 VSCode 还是 PyCharm?
有朋友问 Flet 开发到底用什么编辑器,我的个人经验是:VSCode 和 PyCharm 都能用,重点不在编辑器本身,而在于 Python 解释器是否选对。
- VSCode:需要先在插件市场装好 Python 扩展,然后用命令面板(
Ctrl+Shift+P)选择 Python 解释器,最后指定到你创建的flet_env虚拟环境里,这样语法提示和自动补全才能正常工作。 - PyCharm:社区版就够用,新建项目时可以指定虚拟环境,也可以打开现有项目后在 Settings 里把 Project Interpreter 改成
flet_env的 python 可执行文件。
我平时主力用 VSCode,因为启动更快,界面也更清爽。但如果你更习惯 PyCharm 的调试功能,就直接用 PyCharm,Flet 应用本质上就是个 Python 脚本,任何能把 Python 跑起来的编辑器都没问题。
3. 核心控件与布局实操:写一个功能完整的待办清单 App
3.1 理解 Flet 的页面和控件层级
Flet 有一个很形象的概念:page是整个应用的最底层容器,它相当于一张画布,你往里面 add 的每个控件都是一个元素。为了控制布局,Flet 提供了几个常用的布局容器:
Row:让子控件水平排列,一行从左到右。Column:让子控件垂直排列,一列从上到下。Container:可以理解成“带样式和间距的盒子”,用来控制背景色、边框、圆角、内边距等。Stack:则是“绝对定位式”布局,子控件可以叠加在同一位置,适合做悬浮按钮或浮动提示。
这几个控件熟悉之后,基本就能拼出大部分界面。我建议新手先别急着做复杂项目,先花半小时随便拖几个Text、TextField、ElevatedButton到页面上,感受一下布局的变化,慢慢就有手感了。
Flet 控件的属性命名基本遵循 Flutter 的那一套,比如width、height、padding、margin、border_radius、expand,只要你见过 Flutter 的 UI 描述,一看就能明白。
3.2 设计待办清单的数据结构和交互逻辑
我做一个待办清单(Todo App)来演示,因为它麻雀虽小、五脏俱全,能覆盖输入控件、列表展示、事件处理、状态刷新这几个核心环节,正好是新手入门 Flet 最好的范例。
首先明确需求:
- 顶部有一个输入框和一个“添加”按钮
- 点击按钮后,把输入内容加入列表
- 列表每一项前有复选框,勾选后内容变成删除线
- 每一项后面有一个删除按钮,点击后移除该项
对应的数据结构很简单,用一个 Python 的list来维护待办事项,列表里每一项是一个dict,包含文本内容和完成状态:
todos = [ {"text": "写文章", "done": False}, {"text": "买菜", "done": True}, ]显示的时候用 for 循环遍历这个列表,动态生成一行行的控件,塞到Column里。这里要特别说明一下:Flet 的“状态刷新”是你修改数据后,需要把控件重新 add/update 到页面上,页面才会更新。它不是响应式数据绑定驱动,这一点和 Vue、React 的思路不一样。
3.3 动手实现:完整代码示例
import flet as ft def main(page: ft.Page): page.title = "待办清单" page.horizontal_alignment = "center" page.padding = 20 task_input = ft.TextField( hint_text="输入待办事项...", width=300, on_submit=lambda e: add_task(e), ) todo_list = ft.Column(spacing=10) def add_task(e): text = task_input.value.strip() if not text: return task_row = ft.Row( controls=[ ft.Checkbox( on_change=lambda event, t=task_row: toggle_done(event, t) ), ft.Text(text, size=18), ft.IconButton( icon=ft.icons.DELETE_OUTLINE, on_click=lambda event, t=task_row: delete_task(t), ), ], spacing=10, ) task_row.data = {"text": text, "done": False} todo_list.controls.append(task_row) task_input.value = "" page.update() def toggle_done(event, task_row): # 根据复选框状态切换文本删除线 task_row.controls[1].style = ft.TextStyle( decoration=ft.TextDecoration.LINE_THROUGH if event.control.value else None ) task_row.data["done"] = event.control.value page.update() def delete_task(task_row): todo_list.controls.remove(task_row) page.update() page.add( ft.Row( controls=[ task_input, ft.ElevatedButton("添加", on_click=add_task), ], spacing=10, ), todo_list, ) ft.app(target=main)这段代码看起来短,但已经包含了好几个 Flet 的经典操作:TextField的事件回调、IconButton绑定删除操作、用lambda给回调传参、通过修改控件属性和列表来刷新页面。有一个非常容易踩的坑是:在add_task里创建task_row之后,立刻在Checkbox的on_change里引用了task_row,但这时候task_row还没有全部构造完成。所以我在代码里用t=task_row作为 lambda 默认参数把当前行绑定进去,这样事件触发时能拿到正确的行对象。
3.4 运行并观察 UI 更新的逻辑
运行这个脚本,你会看到窗口里是一个简简单单的两列布局。输入内容、点击添加后,todo_list.controls增加了一个Row,然后调用page.update(),界面立刻刷新。勾选复选框时,event.control.value就是当前复选框的选中状态,用这个值去切换删除线。删除按钮则直接按行对象为维度,从列表里移除。
我刚开始学 Flet 的时候,总会下意识把数据和控件分得特别清,但其实在这个框架里控件本身就是数据的一部分,尤其是你需要在界面里动态增删条目时,直接操作Column.controls是最省心的做法。
4. 打包 APK:把 Python 应用装进手机
4.1 Flet 打包的整体流程说清楚
Flet 应用最终是构建在 Flutter 之上的,所以打包 APK 并不是像 PyInstaller 那样直接把 Python 解释器一起打进去,而是借助 Flet 提供的构建工具,把 Python 应用托管到一个嵌入式 Python 环境里,再通过 Flutter 工程打包。
想打包 Android APK,电脑上必须装以下东西:
- Python 3.8+,这是跑 Flet 代码的基础
- JDK 17,Android 打包链路的必要组件
- Android SDK,至少包含 platform-tools 和 build-tools
- Flutter SDK,Flet 底层调用 Flutter 构建命令
如果你从来没碰过 Flutter,装 SDK 这块可能会有点懵。我建议按官方文档一步步来:先装 JDK,再装 Android Studio(顺手把 SDK 装了),最后装 Flutter 并把它加入 PATH。用命令行执行flutter doctor检查环境是否齐全,直到不出现红叉。
4.2 实操:flet build apk 命令怎么用
进入你的 Flet 项目目录,运行:
flet build apk如果一切顺利,稍等几分钟(第一次打包需要下载依赖,会比较慢),就能在build\apk\release目录下看到app-release.apk文件。这个文件直接传到手机上就能安装,不需要额外签名。
但我必须提前打预防针:第一次打包大概率不会一遍过,绝大多数报错集中在 Flutter 环境和 Gradle 配置上。
4.3 热词里那个长长的报错:Flutter Gradle 插件问题
热搜词里有一条:
you are applying flutter's main gradle plugin imperatively using the apply script
我没记错的话,这个报错在 Flet 的某些版本和 Flutter 新版本组合时非常常见。原因大概是 Flutter 新版对 Gradle 插件的加载方式做了调整,不再推荐在settings.gradle或者build.gradle里用apply命令“命令式”地应用插件,而是要求用插件 DSL 的plugins {}块来声明式地加载。
遇到这个报错后,我的处理思路是:
- 找到 Flutter 工程里
android/settings.gradle和android/build.gradle,看里面是否有apply相关命令。 - 按照 Flutter 官方模板调整成新版声明式写法,通常是把
apply plugin: 'com.android.application'替换成自定义plugins块。 - 如果不想手动改,可以把 Flet 升级到最新版,再重新执行
flet build apk,因为新版本通常已经适配了新版 Flutter SDK 的构建方式。
这里有一个很实用的技巧:打包前先看 Flet 官方文档里对 Flutter 版本的要求,别用最新版的 Flutter,因为新 SDK 刚发布时往往会出现兼容性窗口期。我在实际项目中使用的就是官方文档指定的稳定版组合,这样打包基本一路顺畅。
4.4 iOS 和 Web 打包:一个一次性搞定,一个需要 macOS
除了 Android,Flet 也支持 iOS 打包和 Web 部署。
- iOS:前提是你有一台 macOS 电脑,并安装 Xcode。然后执行
flet build ipa,能生成 IPA 包(模拟器版或真机版)。如果你不是 iOS 开发者,也没有 Apple 开发者账号,签名和上架这块会比较麻烦,可以先用模拟器调试。 - Web:执行
flet build web,会在build/web目录下生成一套纯前端静态文件,包括 HTML、CSS 和 JavaScript。你可以把整个目录扔到 Nginx、GitHub Pages 或任意静态服务器上,就能得到一个可以在浏览器里访问的网页版应用。访问设备不需要装任何环境,打开浏览器就能用,这一点在演示和宣传时特别有优势。
4.5 多线程更新 UI 的正确姿势
热搜词里也有“flutter 多线程”。Flet 里要处理耗时任务,如果直接在事件回调里做复杂计算或网络请求,页面会卡住,体验很差。正确做法是把耗时任务放到单独线程里,同时保证 UI 更新在主线程完成。
Flet 提供page.run_thread方法,它专门用来在线程中执行函数并安全地更新页面。举个实际例子,我做过一个从网上爬取数据并展示的小工具,思路是:
import time import flet as ft def main(page: ft.Page): status_text = ft.Text("准备中...") page.add(status_text) def heavy_task(): time.sleep(3) status_text.value = "计算完成!" status_text.update() def start_task(e): status_text.value = "运行中..." status_text.update() page.run_thread(heavy_task) page.add(ft.ElevatedButton("开始", on_click=start_task)) ft.app(target=main)这个写法看起来很简单,但有一个很容易被忽略的点:线程函数内部去修改控件属性后,必须调用update()或让页面整体刷新,否则界面不会发生变化。而且注意,page.run_thread内部实际上处理了线程和 UI 间的调度,比你自己直接写threading.Thread要安全得多。如果非得自己开threading.Thread,那你在线程里更新 UI 时一定要小心,尽量避免并发修改同一个控件,以免出现乱七八糟的界面状态。
5. 常见问题与排查技巧实录:从入门到打包的速查表
5.1 我在实际项目中踩过的高频问题
这里整理一份 Flet 开发过程中的问题速查表,都是我在真实项目里遇到过、并且从网上翻资料验证过的,不是纸上谈兵:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
ModuleNotFoundError: No module named 'flet' | 没安装 Flet 或没激活虚拟环境 | 执行pip install flet,确认命令行前缀有(flet_env) |
| 启动页面后窗口不显示 | 桌面环境不支持 | 改用flet run --web hello.py |
| 点击按钮没有反应 | 回调函数没加event参数 | on_click回调固定有一个参数,建议写成def handler(e): |
| 界面刷新后控件空白 | 新控件没进行update() | 手动调用page.update()或控件自身的update() |
flutter doctor提示 Android licenses not accepted | 还没接受 Android SDK 协议 | 执行flutter doctor --android-licenses |
| 打包时提示 Gradle 下载超时 | 网络访问 Gradle 仓库不稳定 | 根据报错配置镜像仓库或重试,等待时间加长 |
flet build apk报 Java 版本不对 | JDK 版本不兼容 | 切换为 JDK 17,并在环境变量中确认java -version |
| 打包后 Apk 体积过大 | 包含了多架构的 Flutter 引擎 | 可在构建配置里限制targetPlatforms,比如只保留arm64-v8a |
5.2 网络问题和依赖下载问题的排查思路
国内开发者在执行flutter build或flet build apk时,最常见的卡点就是网络,尤其是首次构建要下载大量的 Gradle 依赖和 Flutter 引擎产物。如果你遇到下载缓慢或超时,可以先确认网络是否稳定,然后考虑:
- 检查是否需要配置镜像仓库。Flutter 官方文档提供了中国地区下载 Flutter SDK 的镜像地址,Android 的 Gradle 仓库也可以通过配置全局
init.gradle或项目内build.gradle来指向可用镜像。 - 设置更长超时时间或增加重试次数。Gradle 构建时偶尔会因为某个依赖没拉全而失败,多试几次有时就能过。
- 如果构建总是卡在同一个依赖上,可以手动下载该依赖并放到本地的 Gradle 缓存目录。这个操作比较麻烦,但确实是解决“死局”的最后手段。
5.3 调试思路:学会看 Flet 的日志
Flet 应用的运行日志是比较友好的,出错时多数情况会直接打印 stack trace。遇到问题先别急着搜网,养成看日志的习惯:
- 看报错的第一行和最后一行,那通常是最真实的错误原因。
- 如果报错和按钮回调有关,去检查回调函数里是不是有变量引用不对,或者控件对象被重复替换。
- 如果报错和页面刷新有关,检查是否在控件创建后忘了
update()。
我记得有一次,我写一个表单,点击“提交”后页面没有任何反馈,费了半天劲才发现是回调函数签名少了一个参数,导致根本没有调用成功。从那以后,凡是页面无反应的,我都先怀疑回调函数。
5.4 当你卡住时,哪里找帮助最高效
Flet 的社区目前还算活跃,但中文资料相对少。我常用的求助路径:
- Flet 官方文档:虽然是英文,但结构清晰,控件参考也很全,遇到某个控件不知道怎么用,直接去查对应页面。
- GitHub Issues:特别是报错信息里包含 Flet 或 Flutter 关键词时,GitHub 上基本都能搜到类似问题,里面往往有官方维护者的回复。我遇到过的 Gradle 报错就是在 Issue 里找到的解决方案。
- Stack Overflow:搜索 “flet” 关键词,已经有不少问答了,查问题时记得把关键词从宽到窄都试一遍。
- Flutter 相关社区:由于 Flet 底层是 Flutter,很多 UI 布局、打包报错的处理经验其实是通用的。你用 Flutter 的关键词去搜,常常能获得比 Flet 更丰富的资料。
6. 小技巧与我的心得体会
6.1 把这个项目继续扩展的方向
待办清单只是起点,你完全可以在 Flet 基础上继续做更多应用:
- 做一个个人记账本,用图表展示每月的收支变化
- 做一个定时提醒工具,用
page.run_thread动态更新页面 - 做一个内部工具平台,把公司常用的运维脚本、爬虫工具聚合成一个带界面的 Web 服务
- 做一个考勤打卡页面,连上后端 API 后,就能在手机和浏览器上同时使用
Flet 的 Web 版本尤其适合快速交付内部工具:后端逻辑用 Python,前端界面用 Flet,部署到服务器后团队成员只要打开浏览器就能用,不用装任何客户端。我做过最顺手的一个内部小工具,从想法到上线只花了一个晚上,用户体验虽然谈不上惊艳,但胜在实用和轻量。
6.2 我的真实体验:别再纠结“不懂前端”这件事
回到题目那句话:“不懂前端也能写 App”,这句话在我身上基本是成立的。我写了几年 Python 脚本和爬虫,对 JavaScript 和 CSS 始终提不起兴趣,但 Flet 让我第一次感到“做界面也还挺轻松的”。尤其是它的热重载效果,改完代码页面能实时刷新,开发节奏比我想象中快很多。
如果你也想尝试跨界做点带界面的工具,我的建议是:先从最笨的办法开始,照着文档写一个最小示例,然后一点点往里面加功能。别一开始就规划庞大架构,先让一个按钮真正“点得动”,再考虑更复杂的事。这样每完成一个微小闭环,都会增加一点信心,而自信心往往是学习新东西最重要也最容易忽略的因素。
6.3 最后一个提醒:留意 Flet 与 Flutter 版本的兼容性
最后再分享一个我自己用真金白银换来的经验:Flet 版本和 Flutter 版本之间是有对应关系的,升级别太激进。我遇到过一次升级 Flet 后,原本好好的项目突然打不了包的情况,查了一整天才发现是它内部指定的 Flutter 版本和本地 Flutter SDK 对不上。后面学乖了,每次升级前先看 release notes,升级完立刻跑一遍flet build apk验证,不给自己留过夜的问题。
如果你也是刚入坑 Flet,愿你第一次打包就顺利生成那个属于自己的 APK。真遇到报错也别慌,照着日志一行一行排查,所有的坑都会变成你以后的经验。