1. 先搞清楚Kivy到底适合做什么不适合做什么1.1 一套Python代码三端通吃但它不是套壳网页我第一次认真用Kivy是有个内部工具需要在手机上跑但团队只有Python背景完全没人力去写Java/Kotlin那套原生逻辑。当时在Flutter、React Native和Kivy之间犹豫了一周最后选了Kivy理由很简单我们不想维护两套UI代码也不想引入一整套JS/TS工程化链路。Kivy能让我们用最熟悉的Python语法在同一套代码里把Android、iOS、Windows、macOS全包住。这里要先把Kivy的本质说清楚它不是一个把网页包一层壳的Hybrid方案比如Cordova那种。Kivy是一个真正的原生UI框架自己绘制每一个控件底层用OpenGL ES做渲染事件循环、触控处理、布局计算全在自己这套引擎里跑。这意味着你写出来的界面不是HTML/CSS的模拟而是Kivy自己的一套定义语言kv语言驱动的真实控件树。你接触到的每个Button、Label、TextInput在屏幕上就是由Kivy自己画出来的图形不是某个WebView渲染的结果。这对跨平台开发是有实际意义的同一套代码在Android和Windows上跑出来的交互逻辑一致不会出现手机上是网页手感、桌面上是原生手感的分裂感。而且因为所有控件都是自绘你可以非常精细地控制视觉和交互细节做到像素级一致。1.2 预期管理性能边界与原生能力缺口但我也必须泼一盆冷水。Kivy在移动端的性能边界是真实存在的它适合做工具类、业务类、数据展示类的应用不适合做游戏引擎级别的复杂实时渲染。虽然Kivy有canvas指令可以画图、有Clock可以做帧循环但它没有Game Engine那套物理、碰撞、动画补间系统。你非要用它做飞行射击游戏最终会卡在性能优化上。更实际的问题在原生能力。Kivy本身不直接提供摄像头扫码、蓝牙BLE、NFC、高精度定位这类系统级API这些都要靠配套的plyer库或者写Android的pyjnius桥接/Python-for-Android的Recipe。如果你要做的App主线功能强依赖这些原生能力最好先确认对应的库有人维护、能打包成功再决定立项。我在做项目时遇到过蓝牙模块在Android 13上崩溃最后是花了一周改pyjnius调用才解决的这种成本要提前算进去。1.3 谁适合学Kivy人群判断说了这么多适合用Kivy的人其实很清晰已经会用Python处理业务逻辑想快速做出一个能在手机上运行的MVP目标平台以Android为主界面复杂度中等偏下。学生做课程设计、企业内部工具、个人效率应用、原型验证这几个场景Kivy的性价比非常高。反过来如果你的目标是上架Google Play做商业产品、需要大量原生动效和极致流畅度Kivy就不是最优解建议去看Flutter。我这个判断不是凭空说的。Kivy的打包生态里Python-for-Android对Android的支持是成熟的iOS打包要么用Kivy官方的kivy-ios要么走其他桥接方案过程明显更折腾。所以实务里大多数Kivy项目都是Android为主桌面做辅助调试iOS更像是理论上支持。你现在想学Kivy我建议直接把它当成一个用Python写Android应用的工具来规划桌面端能跑能调试就是胜利。2. 环境搭建与第一个能跑的App超乎想象的顺利也超乎想象的啰嗦2.1 安装pip之外的注意事项Kivy的安装是我见过Python框架里比较传统的直接pip install kivy在Windows和macOS上官方提供了预编译的wheel包装上就能跑。但如果你是Linux用户尤其是Ubuntu这种依赖系统OpenGL库的环境经常需要额外装一堆东西sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev这些都是底层渲染、音频、视频解码要用的库。初次安装时很容易漏掉它们结果一运行程序就直接Segment Fault或者窗口黑屏。我的经验是先跑一个最简单的窗口程序如果黑屏优先查显卡驱动和OpenGL支持Kivy对OpenGL ES 2.0的依赖是硬性的。另外如果只用桌面端开发和调试建议顺手装:pip install kivy[base] kivy[media]base包含核心控件和布局media包含音频视频播放支持。不装media直接跑某些示例会报No module named kivy.lib.ffmpeg之类的错。这个坑在官方文档里提示过但很多人不看。2.2 第一个Hello World理解事件循环才是关键装完之后新建一个main.pyfrom kivy.app import App from kivy.uix.label import Label class HelloApp(App): def build(self): return Label(textHello Kivy) if __name__ __main__: HelloApp().run()运行后你会看到一个窗口上面显示一行文字。代码很简单但我建议你停下来想一件事run()这行做了什么Kivy的App.run()会启动一个无限事件循环它的任务包含三块处理底层窗口系统推送的事件触摸、鼠标、键盘、窗口大小变化、遍历并更新Widget树、触发Canvas重绘。整个App的一切行为都在这个循环里发生。所以你在Kivy里做动画、做定时任务、做网络回调后更新UI都必须回到这个事件循环的框架内思考。这也是新手最容易犯的错你在某个回调里写了一个time.sleep(3)结果界面冻结3秒。因为time.sleep把整个事件循环挡住了渲染也要等3秒。你在Kivy里想要延迟做某事应该用Clock.schedule_once而不是sleepfrom kivy.clock import Clock def do_something(dt): print(3秒后执行) Clock.schedule_once(do_something, 3)理解了这个事件循环模型后面调试各种界面卡住回调不触发的问题才有清晰的排查方向。2.3 调试三件套窗口重载、日志、模拟触摸Kivy桌面端的调试体验其实是够用的标配是这三样第一热重载。Kivy有一个kivy-modules的reload模块不过我更推荐直接改完代码重新运行因为Kivy的App状态重载容易残留旧Widget状态反而更难排查。真正高效的方式是把界面逻辑写进kv文件改kv文件后按F5就能重载界面这个体验很好。第二日志输出。Kivy默认会在控制台打印日志信息量很足。点击一个按钮你会看到Button的事件调用栈资源加载失败会明确告诉你哪个文件找不到。写代码时我不建议关闭日志信息量与性能损失完全值得。第三模拟触摸。桌面端没有触摸屏但Kivy的模拟器可以在调试时模拟多个触点按住鼠标左键拖动是单点触控按住右键画圈是会缩放/旋转。很多手势问题在桌面端就能初步复现不用每次跑到手机上看。我还习惯在开发早期就打开一个全局的Window.bind(on_keyboard...)把电脑键盘的Esc键绑定为退出调试模式。这个小技巧在真机调试时会帮你快速关闭App窗口省得频繁找退出按钮。3. kv语言与Widget树理解Kivy的心智模型比记API重要3.1 为什么要kv文件界面与逻辑分离不是口号Kivy最独特的语法是kv语言。一个典型的kv文件长这样#:kivy 2.2.0 LoginScreen: BoxLayout: orientation: vertical TextInput: id: username hint_text: 用户名 TextInput: id: password password: True hint_text: 密码 Button: text: 登录 on_release: root.login(username.text, password.text)如果你直接在Python里用BoxLayout()、TextInput()拼界面当然也行。但项目一大Python代码里混着布局、样式、事件回调很快就会变成一团乱麻。kv文件把界面结构和逻辑分开好处很直接视觉结构一目了然缩进层级就是控件树层级修改布局不用翻Python代码kv文件天然支持正则、模板可以定义重复使用的组件。但要注意kv文件不是简单的静态描述它的每一行在运行时都会被Kivy语法解析器解析最终生成真实的Python对象。所以kv文件和Python代码之间是共生关系不是两套独立的系统。3.2 布局选择BoxLayout、GridLayout、AnchorLayout和FloatLayout的实战区分这是Kivy初学者最容易迷迷糊糊的地方。布局容器决定子控件怎么排列选错布局后面所有尺寸和位置都在打架。我的选型经验如下BoxLayout按水平或垂直方向排列最适合做从上到下/从左到右的区域划分比如顶部标题栏中间内容底部按钮。GridLayout按行列网格排列适合表单、九宫格、键盘这类规整布局。要注意它在子控件数量和列数不匹配时行为很反直觉建议先用代码算好格子数再排。AnchorLayout把子控件锚定在容器九个位置之一左上、中上、右上、中心等适合做悬浮按钮角落的角标。FloatLayout完全自由定位依赖pos_hint和size_hint适合做复杂自定义界面但也最容易失控新手建议少用。StackLayout按流动方向排超出边界自动换行适合标签组、不定长按钮流。实际项目里几乎都是组合使用外层一套BoxLayout内部用一个GridLayout做表单右上角用AnchorLayout放一个刷新按钮。只要思路是分区块再嵌套界面设计就不会太乱。有个参数值得掌握size_hint。它表示子控件占父容器比例取值范围0到1。如果你想让某个控件固定在40像素高度要写成size_hint_yNone加height40。这个先禁用比例再设固定值的写法用得极其频繁不打出来你后面每个控件都会遇到尺寸难题。3.3 属性绑定与事件不是赋值是联动Kivy的Property机制是我认为它最有含金量的设计。在Kivy里你可以给自定义Widget定义属性from kivy.properties import StringProperty class LoginScreen(FloatLayout): status StringProperty(未登录)这个status不是普通Python字段而是一个带状态追踪的属性。当你修改self.status 登录成功时Kivy会通知所有依赖它的地方刷新。在kv文件里可以这样用LoginScreen: Label: text: root.status这行text: root.status建立的是一种绑定关系status变化时Label的text自动更新。这是响应式编程的思路跟我后面会用到的前端Vue、React的响应式状态很像。好处是你永远不需要手动写更新这个Label的代码状态一变界面自己就变了。进阶用法是自定义事件。Kivy的Widget都有事件机制你可以在自己的类里定义新事件from kivy.properties import ObjectProperty from kivy.uix.boxlayout import BoxLayout class LoginScreen(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type(on_login_success) def do_login(self, username, password): if username admin and password 123: self.dispatch(on_login_success, username) else: self.dispatch(on_login_failed) def on_login_success(self, username): pass def on_login_failed(self): pass然后在外部绑定screen LoginScreen() screen.bind(on_login_successlambda widget, username: print(登录成功, username))Kivy里每个控件都是一个事件源理解了这个模型你做复杂交互时才有底气回调之间不直接耦合而是通过事件解耦。4. 动手做一个任务清单从UI到交互的完整实践4.1 需求与结构设计前面都在打基础这个部分我用一个任务清单App做例子把从零到能用的完整过程讲一遍。功能需求很简单添加任务、删除任务、显示任务列表、支持滚动。参与流程最直观先想清楚数据结构再搭界面最后接交互。数据结构我用Python列表就行每项一个字典包含标题和完成状态tasks [ {title: 写Kivy博文, done: False}, {title: 测试Android打包, done: False}, ]界面规划成三层顶部一个TextInput加添加按钮中间是RecycleView显示列表底部可选一个清空已完成按钮。整个根布局用BoxLayout垂直方向。4.2 用RecycleView显示列表别再用ListAdapterKivy历史上有过一个叫ListView的控件性能差、维护不积极已经被官方方案RecycleView取代。RecycleView的核心思想是复用视图它只创建可见区域的Item实例滚动时反复把数据填充到已有的Item里而不是为每条数据创建一个控件。实现RecycleView需要三件套数据源、视图类、布局管理器。最简写法如下from kivy.app import App from kivy.uix.recycleview import RecycleView from kivy.uix.recycleview.views import RecycleDataViewBehavior from kivy.uix.label import Label from kivy.properties import BooleanProperty, StringProperty class TodoItem(RecycleDataViewBehavior, Label): index None title StringProperty() def refresh_view_attrs(self, rv, index, data): self.index index self.title data[title] return super().refresh_view_attrs(rv, index, data) def on_touch_down(self, touch): if self.collide_point(*touch.pos): self.parent.parent.data[self.index][done] True return super().on_touch_down(touch)对应的kv文件TodoItem: text: self.title size_hint_y: None height: 50 TodoScreen: RecycleView: id: task_list viewclass: TodoItem data: self.tasks_data这里的data: self.tasks_data是绑定关系当你在Python侧更新tasks_data这个ListProperty时RecycleView会自动刷新显示。TodoItem本身只是一个Label但它有index属性知道自己对应数据列表里的哪个位置这样点击事件才能正确改到数据源。RecycleView的第一个坑在viewclass它接收的是类名字符串比如TodoItem而且这个类必须在kv文件里已被定义。如果你写成viewclass: TodoItem不带引号解析器会在全局命名空间里找不到这个类直接报错。这类错误看日志才能反应过来写的时候得很小心。4.3 添加、删除、弹窗与持久化添加任务的逻辑绑定按钮的on_release事件def add_task(self): text self.ids.input_field.text.strip() if not text: return self.tasks_data.append({title: text, done: False}) self.ids.input_field.text self.ids.task_list.scroll_to_end()这里有个细节Kivy里的ListProperty.append不会自动触发绑定刷新你需要给tasks_data做一次完整赋值或者在数据变化后手动刷新视图def add_task(self): ... self.tasks_data [dict(item) for item in self.tasks_data]我用后一种方式的频率更高避免深拷贝时记得保证数据结构简单。如果只是改某个字段也可以直接操作下标self.tasks_data[index][done] True然后手动刷新RecycleViewself.ids.task_list.refresh_from_data()删除任务可以做成滑动删除但为了内容清晰我直接提供一个删除按钮。这里需要注意事件冒泡TodoItem自身绑定了on_touch_down如果再放一个Button子控件点击事件会同时触发两层逻辑所以要做collide_point判断或者stop()处理。我实战中更倾向于保持Item简洁不做嵌套控件而是用长按弹确认的方式来删除交互不复杂而且不易误触。持久化可以用json把任务存到本地。Android上Kivy获取应用私有目录的方式是from kivy.utils import platform if platform android: from android.storage import app_storage_path save_path app_storage_path() else: save_path ./存JSON文件就好不要试图用SQLite除非数据量特别大。任务清单这种轻量数据用JSON足够。5. Android打包全记录从buildozer到APK的实操复盘5.1 环境准备别在Windows上硬刚直接上Ubuntu或WSL桌面环境跑通之后真正的考验在打包。Kivy Android打包的标准工具是buildozer它是一个自动化脚本负责拉取Python-for-Android的SDK、NDK、Python源码然后交叉编译出APK。它的工作环境要求Linux、Python 3.8以上对Windows没有官方支持Windows用户我强烈建议用WSL的Ubuntu或者干脆装个VirtualBox跑Ubuntu 22.04。我第一次在WSL里用Buildozer打包踩的第一个坑是缺一些系统包sudo apt install -y git zip unzip openjdk-17-jdk \ python3-pip autoconf libtool pkg-config zlib1g-dev \ libncurses5-dev libncursesw5-dev libtinfo5 cmake \ libffi-dev libssl-dev然后安装buildozerpip install --user buildozer初始化项目buildozer init这会在项目目录下生成buildozer.spec所有打包配置都在这个文件里。5.2 buildozer.spec关键配置项目详解buildozer.spec有上百行注释我实际关注的就那几个[app] title TodoApp package.name todoapp package.domain org.example.todoapp source.dir . source.include_exts py,png,jpg,kv,atlas,json version 1.0.0 requirements python3,kivy orientation portrait fullscreen 0 android.api 33 android.minapi 21 android.archs arm64-v8a, armeabi-v7a android.allow_backup True android.permissions INTERNETrequirements是打包的核心你用了什么Python库就列什么比如用了请求库就写成requirements python3,kivy,requests,pyjnius。buildozer会根据这个清单去交叉编译相应的Python模块到Android上。android.archs我写的是arm64-v8a和armeabi-v7a覆盖主流手机。如果你要减小APK体积可以先只打arm64-v8a现在新手机基本都是这个架构。orientation portrait锁定竖屏任务清单类应用不需要横屏锁死能少很多布局适配的麻烦。android.api 33是Android API级别buildozer会下载对应的SDK platform。建议跟官方最新稳定版保持一致不要太高也不要太低太新容易遇到SDK组件还没适配的编译问题。5.3 常见打包错误排查与优化打包命令很粗暴buildozer -v android debug第一次打包会下载大量依赖包括整个Android SDK和NDK在国内网络环境下可能要跑一两个小时。我建议先设置好代理镜像或者干脆把同网络下已成功的buildozer缓存目录整个拷贝到新环境这个经验能救很多人。错误1No module named buildozer检查是不是用了pip install --user导致命令行找不到用python -m buildozer代替。错误2Unpacking stage: unsupported or invalid NDKbuildozer有默认NDK版本要求要么按提示改用指定版本要么修改buildozer.spec里的android.ndk。错误3You must install openjdk或者Failed to find java检查Java版本。buildozer对Java版本非常敏感OpenJDK 17通常兼容但也有人遇到17不行必须回到11的情况。我习惯先用17不行再降级。错误4SDK License not acceptedbuildozer会在~/.buildozer/android/platform目录下载SDK第一次需要手动同意许可证。进入对应SDK目录用$JAVA_HOME/bin/java -jar android.jar update sdk --no-ui之类的方式接受协议或者在buildozer.spec里设置android.accept_sdk_license True。打包成功后生成的APK在bin/目录名字类似todoapp-1.0.0-arm64-v8a_armeabi-v7a-debug.apk。连上手机开USB调试直接adb install bin/todoapp-1.0.0-arm64-v8a_armeabi-v7a-debug.apk装完你会发现App启动速度比桌面慢不少这是Python解释器的固有开销。要优化启动可以开启android.entrypoint的自定义启动Activity做热启动或者使用buildozer支持的多架构打包再配合第三方加固工具。但对多数内部工具来说能跑起来比启动快0.5秒更值得在意。6. 性能与坑跑通之后你必须面对的四个真问题6.1 滑动列表卡顿的根源与排查方法任务列表只有十几条时RecycleView怎么滑都不卡。但到几百条、上千条我开始感受到明显的掉帧。原因有两个一是每个Item里的文本渲染开销二是Python侧频繁触发属性绑定的开销。最典型的性能杀手是在RecycleView的data里塞了太多嵌套字典导致每次刷新都触发大量属性绑定。我去翻Kivy官方Issue常见建议是尽量把Item的样式写成常量不要在refresh_view_attrs里动态创建大量对象文本控件不要设置过于复杂的font_size和line_height避免重排如果数据量超过3000条建议分页加载不要一次性全塞给RecycleView避免在on_touch_down和绑定回调里做耗时操作文件读写、网络请求都不行。我用buildozer -v android debug打包后在低端Android手机上实测200条任务列表能顺滑滑动1000条时明显掉帧。如果真要处理大数据量可以考虑用RecycleView的viewclass替换成RelativeLayout自绘Item或者把文本渲染交给纹理缓存。但说实话对于实用工具类App你更应该做的是数据分页或者限制显示数量而不是无脑堆性能优化。6.2 中文字体与图片显示最容易踩的坑Kivy默认字体不支持中文。在Windows桌面跑的时候你可能觉得明明能显示中文啊那是因为Kivy在Windows上会自动检测系统字体。但Android打包之后默认字体换成系统自带的Roboto中文就会显示成方框。解决办法是强制加载中文字体。你需要准备一个.ttf或.otf字体文件我用的是思源黑体的Subset版本体积小放在项目目录里然后在App入口设置全局字体from kivy.core.text import LabelBase LabelBase.register(nameMyFont, fn_regularfonts/SourceHanSansCN-Regular.otf)kv里这样用MyLabel: font_name: MyFont注意这个注册必须在任何文本控件创建之前执行最好放在App.__init__里而不是build()里。否则已经创建的Label不会自动切换字体。图片方面Kivy对PNG和JPG支持没问题但图片分辨率太高时内存暴涨。Android端一张1920x1080的图作为背景实际渲染在大多数手机上都会卡顿。我的做法是所有图片在打包前用工具压缩到目标分辨率的一半能明显缓解内存压力。6.3 网络与多线程不要在UI线程里做IOKivy App如果直接在主线程里跑网络请求比如requests.get()界面会直接卡死。因为UI线程阻塞在IO上Kivy的事件循环就停了。正确做法是用Python的threading或者concurrent.futures把IO扔到后台线程然后通过Clock.schedule_once把结果更新回UI线程。import threading from kivy.clock import Clock def fetch_data(): result perform_http_request() Clock.schedule_once(lambda dt: self.update_ui(result)) threading.Thread(targetfetch_data, daemonTrue).start()这个模式我用得非常多。它在Android上没问题但要注意销毁App时后台线程可能还会跑要设成daemon线程避免进程无法退出。Kivy的App.stop()只在主线程调用才有意义后台线程里你不能直接调用App.get_running_app().stop()得通过Clock.schedule_once回到主线程再停。6.4 构建体积、签名与发布准备buildozer android release是签名版本会要求你配置buildozer.spec里的android.release_artifact aabGoogle Play 上架要求AAB以及android.sign_mode和签名文件。Debug版APK体积通常在20MB到50MBRelease版如果启用了ProGuard混淆和R8压缩能压到15MB左右。我建议在正式发布前把requirements里没用的库全部删掉每多一个库至少增加几MB体积还可能引入编译失败风险把图标和启动画面在buildozer.spec里配置好不要用默认图标不然第一印象很差在android.permissions里只声明你实际用到的权限多一个权限就多一分审核风险。签名是必踩的一次坑buildozer android release默认会生成一个debug签名但它只能用于测试上架时必须换成正式keystore。配置文件里android.sign_mode release android.signakey /path/to/keystore.keystore android.signalias mykey android.signkey_pass password这些敏感信息建议用环境变量注入不要把密码明文放在项目里。虽然buildozer.spec会被本地引用但它也可能被打进构建记录里。我个人的习惯是项目一开始就把buildozer.spec里与用户隐私、签名相关的配置单独抽成一个本地文件不提交到Git仓库。这算是被坑过一次后的条件反射。最后说一点方向上的心得。Kivy这三年给我的感觉是稳定、小众、够用。它不会像Flutter那样让你惊艳也不会像RN那样有海量组件库但如果你和我一样是纯Python生产力选手Kivy几乎是唯一能在不学第二门语言的前提下把手机App这件事真正做完整的路径。用它处理内部工具、课程设计、原型验证这套学习曲线换来的时间回报是非常值得的。如果你真做出了一个跑通的MVP再去考虑是否要为了性能换Flutter那时候你已经知道自己需要什么了。