十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Kivy实现Python跨平台App开发:从环境搭建到APK打包实战

用Kivy实现Python跨平台App开发:从环境搭建到APK打包实战 先说句实在的用Python写手机App在很多人的印象里就是不靠谱、跑不通、就算跑通也卡成PPT。我一开始也是这么想的。但连续折腾了三个项目之后我不仅用Kivy把Android的APK打出来装到了自己手机上还把同一个代码库跑在了Windows桌面和Linux开发机上。这篇东西不是官方文档的翻译是把我从环境搭建、KV语言、界面布局、音乐播放、buildozer打包到真实手机上的整个链路都过了一遍之后的记录中间踩过的坑、最后验证出的可行路线都会写清楚。如果你是被大作业逼着用Kivy做移动应用开发或者手里正好有需求要做一个跨平台的工具型App这份记录应该能帮你少走很多弯路。1. 先说结论Kivy适合什么样的项目、什么样的人很多人会拿Kivy和Flutter、React Native对比其实它们完全不是一类东西。Flutter和React Native最终会把界面渲染成Android/iOS原生的控件体系用户摸到的是原生感而Kivy本身就是在OpenGL ES上自己绘制画布按钮、输入框、滑块这些控件都是Kivy用Python和OpenGL画出来的不依赖系统控件。这意味着同一套代码在Android和Windows上显示出来几乎一模一样代价是它的控件观感跟原生Material Design或Cupertino风格有明显差异。换句话说用Kivy做那种看起来必须很原生的应用你会一直跟自己的预期较劲越做越累。但反过来如果你的核心需求是快速验证一个工具型App、内部管理系统、课程设计Demo并且团队主力用的是Python那Kivy是非常顺手的方案。它有完整的Widget体系、声明式的KV语言、布局容器还有自带的手势与多点触控支持。尤其是你要做数据展示、后台调接口、本地文件管理这类逻辑不复杂但界面是刚需的项目纯Python一套写完桌面调试完再打包Android整个闭环非常舒服。像热词里常被人搜的跨平台音乐管理系统其实就是这类项目的典型代表。Kivy不适合的场景也得多说几句。极高交互要求的游戏它虽然带了一个简单的游戏引擎思路但物理碰撞、粒子特效这些还是会力不从心、需要大量调用系统原生能力的应用调用相机深度的API、系统级推送、蓝牙协议栈都会比原生麻烦很多这两类我建议你不要选Kivy。你会在找第三方库桥接原生能力的路上耗费大量时间最终得到的还是一个用户体验差一截的东西。我的判断标准很简单凡是主要价值在UI交互和系统能力的项目不要选凡是主要价值在业务逻辑且Python能搞定的Kivy就很香。1.1 Kivy和Python其他移动方案的本质区别Python生态里做移动端其实不止Kivy一个选项。BeeWare有Toga是映射到平台原生控件的一套方案pyqtdeploy能把PyQt转到移动端桌面时代大家都很熟还有直接用Chaquopy把Python代码嵌进Android Studio里的玩法。这几个我都接触过。Toga理念很漂亮但控件的成熟度和移动端适配比Kivy差一截pyqtdeploy配置繁琐官方维护节奏也不像Kivy这么稳定社区里能找到的移动端案例很少Chaquopy则是把Python当作Android应用里的脚本组件UI还是得用Java/Kotlin写属于另一条路线。Kivy在这个位置站住脚说到底靠两个东西一是它从根上就是为触控、多点、跨平台设计的触控事件的抽象做得早双指缩放、长按、拖拽这类手势在移动端很重要Kivy在架构层面优先考虑二是KV语言这套DSL极大降低了界面代码的重复度写界面基本就是声明式地描述布局和事件绑定不用像原生Android那样一个控件new半天再塞进布局。从项目交付的角度看Kivy让你用纯Python完成界面逻辑打包全链路这对一个小团队或单人完成大作业的场景价值非常高。2. 环境配置里的取舍桌面调试环境与Linux打包环境2.1 Windows桌面开发环境30分钟跑起来如果你在Windows上做Kivy开发先别急着装。我建议用一个干净的虚拟环境别污染系统Python。以Python 3.11为例直接这样来python -m venv .venv .venv\Scripts\activate pip install --upgrade pip pip install kivy[full]关于装kivy[base]还是kivy[full]我的建议是直接full。full除了核心库还会带上kivy.garden相关的扩展组件和一些常用依赖省得之后用到什么再回头补反正虚拟环境里多几个包不心疼。国内网络环境下如果直接pip安装很慢加一下清华镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple kivy[full]装完之后第一步不是直接开写界面先跑一个最基础的应用确认SDK初始化没问题。如果运行时报OpenGL相关错误比如OpenGL版本过低或者Failed to load OpenGL很可能你的机器默认走的是核芯显卡或远古驱动。Windows下临时方案是设置环境变量KIVY_GL_BACKENDangle让Kivy走ANGLE这个OpenGL到DirectX的转译层大多数旧机器都能救回来。桌面开发阶段Window.size是一个值得一上来就设置的东西。你开发的是移动App窗口却默认是台式机尺寸很多布局在手机竖屏上会错得离谱。通常在build()方法里或入口处写一句from kivy.core.window import Window Window.size (360, 640)这样桌面窗口就能模拟一部中端Android手机的竖屏尺寸触摸事件的调试也是靠鼠标模拟的左键是单点触摸按住移动是拖拽如果用的是触摸屏笔记本那体验更接近真机。Kivy的鼠标触摸逻辑是SDL2事件层统一处理的所以桌面调好的界面事件到手机上基本不会因为坐标系变形出问题。2.2 为什么打APK必须用Linux环境Kivy打包Android的官方工具是buildozer它底层调用python-for-android而python-for-android对Linux环境的依赖很深。你在Windows里直接pip装buildozer然后执行buildozer android debug大概率会直接报错或者卡在环境检查上。官方推荐做法是用WSL2装Ubuntu在Ubuntu里完成打包。WSL2环境下我建议用Ubuntu 22.04Python版本控制在3.9到3.11之间把下面这一串系统依赖装全sudo apt update sudo apt install -y git zip unzip openjdk-17-jdk autoconf libtool pkg-config zlib1g-dev libncurses-dev libffi-dev cmake pip install --upgrade buildozeropenjdk版本我特意写的是17因为python-for-android对新旧JDK的支持有窗口期21在某些版本里会有Gradle兼容问题autoconf和libtool是编译Python本体和部分native模块的必需品少一个就容易在下载完源码后编译失败。这个环节没什么可偷懒的空间缺了就补。2.3 buildozer.spec里最值得提前弄懂的字段项目根目录下执行buildozer init会生成buildozer.spec里面的注释已经写得比较完整但新手最先踩的坑也都在这里。我把自己调好的几个关键字段列一下配置项推荐值说明title应用显示名手机上安装后显示的名字package.name全部小写英文内部包名不能有连字符package.domain如org.example最终APK包名是两者拼接source.include_extspy,png,jpg,kv,atlas,json,ttf漏了kv或ttf就会运行时找不到文件requirementspython3,kivy用到的Python库一个都不能少orientationportrait 或 all手机锁竖屏选portraitandroid.permissionsINTERNET等缺权限会在运行时才暴露android.archsarm64-v8a新手机基本都是这个架构android.accept_sdk_licenseTrue不接受许可证会被SDK卡住requirements字段是最容易被忽视的。你在代码里import了requests但spec里只写了python3,kivy那么打出来的APK一到真机运行import requests那行就崩。我在音乐管理系统里用到了SoundLoader理论上kivy自带音频模块但有些格式编码在Android上需要额外的ffmpeg编译所以如果音频格式支持不够requirements里要考虑python3,kivy,ffpyplayer之类。真机上崩溃非常难排查一定要保证requirements和实际代码依赖严格一致。3. KV语言声明式界面背后真正要理解的东西3.1 KV文件到底是怎么被加载的Kivy的KV语言很接近CSS加模板的混合体用结构描述界面层级用规则绑定事件与数据。它不是一个运行时逐行执行的脚本而是由Builder在App启动时解析、编译成一个控件树。默认情况下KV文件的名字要和App类对应比如MusicManagerApp自动去找musicmanager.kv去掉App后缀再全小写。也可以写成music.kv然后手动调用Builder.load_file(music.kv)。我习惯用后者因为在Kivy项目里写了一个以上的KV文件时明确加载顺序感觉更可控。KV文件里类名不加尖括号的顶层声明是根规则比如MusicManager: orientation: vertical意思是当Python代码里实例化MusicManager这个类时KV规则会自动把它的子控件填充进去。这和XML布局的inflate思想有点像但更聪明的是它的属性是可以写表达式的比如text: str(root.current_song)当root.current_song这个Python属性变化时这个文本标签也会自动更新背后是Kivy的事件系统在驱动。3.2 最精简的布局与事件绑定模型Kivy布局容器很多BoxLayout、AnchorLayout、GridLayout、FloatLayout、StackLayout各有定位场景。日常写AppBoxLayout纵向套横向基本能解决90%的需求就跟你用CSS写页面时flexbox为所欲为一样。事件绑定有两种路子。一是直接在KV里写on_press: root.scan_music(folder_input.text)这种方式简单直接适合处理按钮点击二是用Python方法绑定button.bind(on_pressself.callback)适合动态创建控件。我的偏好是静态界面的交互全放KV表达式里动态列表项的交互放Python里或动态类里。为什么KV里写逻辑太深会很难调试Python里写太多绑定又让代码冗长两者配合着来。在KV表达式里访问某个具体控件最常用的手段是id。你在TextInput上写个id: folder_input然后在事件表达式里直接写folder_input.text就可以在KV表达式作用域内拿到它的属性。不要通过复杂的父子链条去访问兄弟节点那样代码非常脆。3.3 动态类、列表推导式与RecycleView如果某个控件在列表里要复用成千上万次写成静态规则会爆炸。KV语言提供了动态类比如SongRowBoxLayout: height: dp(48) size_hint_y: None Button: text: root.textSongRowBoxLayout的意思是定义一个名为SongRow的子类父类是BoxLayout这个类的实例可以直接在RecycleView的viewclass中声明。关键是SongRow还在Python里需要有对应的属性用来接收列表数据项里的字段。配合列表数据Kivy 2.x时代最推荐的是RecycleView而不是ScrollView加循环添加控件。RecycleView的思路是把数据和视图解耦你给data属性传入一个字典列表它只实例化当前可见区域那几行控件滑动时复用。这个设计对性能影响极大后面优化部分会专门展开。在KV里可以用列表推导式塞数据RecycleView: id: song_rv data: [{text: name, path: path} for name, path in root.song_list]不过我个人建议把数据处理放Python里做原因很简单KV表达式里做复杂逻辑一旦出问题报错信息没有Python文件那么直观调试成本高。KV负责界面描述Python负责数据加工这条边界划清楚项目越写越舒服。4. 实战案例用Kivy做一个能跑的跨平台音乐管理系统4.1 需求拆分与界面结构我看到热词榜上有跨平台音乐管理系统v2.0源码和移动应用开发大作业正好用这个方向当例子展开。做一个音乐管理系统核心需求通常就三条扫描目录里的音乐文件、列出可播放的歌曲、点击播放并显示当前状态。可能还要有停止、暂停、下一首但最小闭环做到扫描列表播放就算跑通了。我把界面拆成三块顶部是一个TextInput和一个扫描按钮中间是歌曲列表RecycleView底部是一个显示当前播放歌曲的Label和播放/停止按钮。这个界面用KV语言写结构非常清晰。对应Python端我定义一个MusicManager类继承BoxLayout它持有歌曲列表、当前歌曲路径、播放状态和SoundLoader对象import os from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.properties import BooleanProperty, ListProperty, StringProperty from kivy.core.audio import SoundLoader from kivy.clock import Clock class SongRow(BoxLayout): text StringProperty() path StringProperty() class MusicManager(BoxLayout): songs ListProperty([]) current StringProperty() playing BooleanProperty(False) _sound None def scan_music(self, folder): result [] if os.path.isdir(folder): for root, dirs, files in os.walk(folder): for f in files: if f.lower().endswith((.mp3, .ogg, .wav)): result.append((f, os.path.join(root, f))) self.songs result self.ids.song_rv.data [{text: n, path: p} for n, p in result] def play_by_path(self, path): if self._sound: self._sound.stop() self._sound SoundLoader.load(path) if not self._sound: self.current 无法加载文件 return self._sound.play() self.current os.path.basename(path) self.playing True def toggle_play(self): if not self._sound: return if self.playing: self._sound.stop() self.playing False else: self._sound.play() self.playing True class MusicManagerApp(App): def build(self): return MusicManager() if __name__ __main__: MusicManagerApp().run()4.2 KV文件里如何组织这个界面对应的music.kv核心内容是这样#:kivy 2.2 SongRow: orientation: horizontal Button: text: root.text on_release: app.root.play_by_path(root.path) MusicManager: orientation: vertical padding: dp(10) spacing: dp(8) BoxLayout: size_hint_y: None height: dp(48) TextInput: id: folder_input hint_text: 请输入音乐目录路径 Button: size_hint_x: None width: dp(90) text: 扫描 on_release: root.scan_music(folder_input.text) RecycleView: id: song_rv viewclass: SongRow RecycleBoxLayout: default_size: None, dp(48) default_size_hint: 1, None spacing: dp(2) orientation: vertical BoxLayout: size_hint_y: None height: dp(56) Label: text: 正在播放: root.current Button: text: 停止 if root.playing else 播放 on_release: root.toggle_play()这里最需要留意的是SongRow里的on_release: app.root.play_by_path(root.path)。app.root是App的根Widget就是那个MmusicManager实例这是一种不依赖多层parent链条的调用方式。如果写成root.parent.parent.play_by_path(...)一旦布局层级调整代码就废了。另外RecycleView的data里每个字典的key要能对应SongRow的属性text对应文本显示path对应播放调用。4.3 SoundLoader的播放控制与进度同步Kivy的音频支持不像桌面媒体播放器那么豪华但MP3、OGG、WAV这些常见格式是可以的。SoundLoader.load()成功后会返回一个Sound对象play()和stop()都是显式控制。你要特别注意Sound是底层音频播放器的一次性封装每次播放新歌最好先停掉旧的并重新load否则可能引发底层SDL_mixer的状态冲突。如果想做进度条同步可以用Clock定时器轮询def update_progress(self, dt): if self._sound and self.playing: pos self._sound.get_pos() length self._sound.length # 将pos/length同步到ProgressBar.value上面的代码可以放进scan_music或play_by_path里启动一个Clock.schedule_interval(self.update_progress, 0.5)。Clock是Kivy的主线程定时器UI刷新都必须通过它来驱动千万别自己开thread去改UI属性Kivy的控件属性不是线程安全的容易出现诡异崩溃。4.4 真机调试前的细节路径与权限Android上访问sdcard路径比桌面复杂得多。桌面调试时你输入C:\Users\xxx\Music就能扫手机上你需要一个运行时权限申请机制。Kivy本身不直接管理Android运行时权限buildozer.spec里声明权限、并且Android 13、14上需要动态申请。我在实际项目里是用pyjnius调Java的ActivityCompat做动态权限或者更省事的是让用户自己用系统文件管理器把音乐放到App私有目录再扫描。后者对Demo类项目完全够用也不用引入大量桥接代码。5. buildozer打出第一个APK从构建到闪退排查5.1 第一次构建的时间线与依赖全貌第一次执行buildozer -v android debug时间线通常是这样先下载并解压Android SDK、NDK再下载python-for-android需要的各种预编译模块然后用Cython编译Python源码为C再编译成so接着跑Gradle打包。我机器上第一次构建花了四十多分钟主要时间消耗在下载和解压依赖上。第二次之后增量编译会快很多通常五到十分钟。这个过程的日志非常长别指望肉眼盯住每个输出。我建议直接重定向到文件buildozer -v android debug 21 | tee build.log出错时用grep找关键字比如grep -i error build.log。5.2 几个高频构建问题内存不足是最常见的构建失败原因。Cython编译、Gradle守护进程、系统其它服务同时运行2G内存的机器很容易OOM。WSL2里可以在.wslconfig里给大于4G的配置或者临时增大swap。还有一个方案是在buildozer.spec里把Gradle内存调低但这个优先级不高。SDK下载慢要区分情况。如果是首次下载每次卡在Downloading SDK国内建议直接手动下载SDK commandline-tools放入buildozer的缓存目录也可以设置环境变量指向宿主机已有的Android SDK但注意SDK版本和buildozer期望的版本不能差太多否则校验不过。还有一个经典错误是Cython版本不兼容。buildozer默认会装它要求的Cython版本如果你在系统Python里手动装了Cython 3.x某些Kivy旧版和python-for-android处理原生模块时可能报numpy.oldnumeric或是奇怪的C扩展编译错误。我的建议是buildozer装它自己管理的那一套环境别手动给系统环境乱配Cython。5.3 真机闪退后用adb logcat找到真正原因构建成功不意味着结束。我第一次打出来的APK装到红米手机上点开图标白屏一秒就退回桌面印象极其深刻。这个时候Kivy默认不会弹出一个日志框你需要把手机USB调试打开执行adb logcat | grep -E python|kivy|FATAL|AndroidRuntime关键要看的是Python traceback。Kivy在Android上的异常一般会由python-for-android捕获后输出到logcat。最常见的崩溃原因是requirements里少了依赖。比如你代码里from kivy.core.audio import SoundLoader没问题但实际到了Android上如果你用到的音频格式需要ffmpeg支持但requirements没加对应模块加载时就会崩。还有一种是buildozer.spec里的source.include_exts漏了kv文件导致的FileNotFoundError或者Builder加载失败。如果是AttributeError: NoneType object has no attribute xxx多数是KV文件的根控件和Python类没有对应上或者KV路径加载没生效App.build返回了空。这类问题在logcat里信息量很大不要怕刷屏过滤关键字逐行看。5.4 APK体积与权限策略Kivy打出的APK很容易到30MB以上因为Python运行时加上Kivy核心库的体积本来就大。arm64-v8a单架构会比abi2或3小不少但如果你还要支持老设备还需要加armabi-v7a体积又上去了。对个人项目来说我只保留arm64-v8a把android.archs那一行改掉APK体积能明显下降。权限方面buildozer.spec里写的android.permissions是最终进入APK的授权清单。只写INTERNET那访问本地的存储在某些Android版本上依然可能受限。Kivy文档建议运行时权限通过pyjnius处理但Demo级别应用我的经验是音频播放、文件扫描这些操作在App私有目录下基本不需要复杂权限如果一定要扫sdcard公共目录Android版本高了会比较折腾就提醒用户手动授权或者用系统文件选择器。6. 从能用到好用性能、兼容与后续优化方向6.1 为什么列表必须用RecycleView而不是ScrollView循环添加如果你在网上搜Kivy教程会看到老教程里常出现for i in range(100): list_box.add_widget(...)塞进ScrollView。这种做法在二三十条数据时没感觉一旦数据量上千帧率会断崖式下降。因为ScrollView里的每个子控件都是真实存在的全部被SDL渲染器一帧帧画开销巨大。RecycleView通过viewclass机制只实例化可见区域内的那几行数据量从1000变成10000它处理的控件数量基本不变。这跟Android里的RecyclerView和前端虚拟滚动是同一个思路。步骤上就是给RecycleView指定viewclass: 行控件类名然后传入data列表。实测效果用ScrollView堆300行带Button的列表桌面端滑动已经明显掉帧换RecycleView后同样数据量滑动流畅度基本和原生列表无差别。这个优化是大数据量列表应用的必要条件。6.2 图片加载与网络请求的异步处理如果你的跨界应用里要加载封面图或远程数据不能在主线程做网络请求否则UI会卡死直到超时。Kivy提供了一个AsyncImage控件专门做异步加载图片。我在另一个项目里用它加载歌曲封面URL直接传给AsyncImage它会自己开线程下载并回调更新省了很多事。但AsyncImage只解决图片非图片类的网络请求还是得自己处理。我的习惯是用Python的threading模块包一下回调时通过Clock.schedule_once切回主线程再更新UI或者用mainthread装饰器。Kivy里的kivy.clock.Clock.schedule_once是跨线程更新UI的最稳妥手段。这里有个很容易忽略的坑手机端网络请求如果明文HTTPAndroid 9及以上可能直接被系统拦截需要permissions之外还要考虑网络安全配置。更省事的做法是全部走HTTPSDemo阶段这个规范意识越早有越好。6.3 iOS、桌面和树莓派的一体化思路Kivy跨平台这句话不是空话。同一个代码库我在Windows上跑在Ubuntu上跑在树莓派上跑都是同一套KV和Python。这个特性在项目演示时很加分。iOS的打包相对Android麻烦kivy-ios需要Mac和Xcode环境单独维护一套toolchain只为了一个APK级别的验证就没必要折腾。我自己的习惯是主线交付Android APK桌面端作为开发调试环境如果有需要在树莓派这类设备上做信息展示屏、触屏交互终端Kivy也是顺手的选择。这种一套Python逻辑多端复用的模式对个人开发者和课程项目来说省下的时间非常可观。当你习惯了这套工作流之后你会发现Kivy的真正定位不是又一个跨平台玩具而是把Python带入设备端应用层的一条轻快路径。
返回列表