ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python+Kivy 实战:从零开发背单词 App 并打包 Android APK

Python+Kivy 实战:从零开发背单词 App 并打包 Android APK 简介一份基于Python与Kivy框架的背单词App完整项目源码专注于解决程序员日常英语学习需求。项目命名为51斩百词采用Sqlite存储词库、Virtualenv隔离依赖环境从界面设计到打包生成apk均有完整演示适合有一定Python基础、希望入门移动端开发实战的开发者参考。压缩包内共118个文件涵盖61个py源码、20个kv界面描述、图片与音频素材、sql/db词库、使用说明文档以及可直接安装运行的apk成品整体约86.15MB。资源附带Word版使用说明、数据库文件和sql脚本源码中完整呈现词库读取、界面布局与事件处理逻辑可帮助读者拆解Kivy应用组织方式理解Python如何调用原生组件并完成数据持久化还能在既有词库基础上自定义扩展作为课程设计或个人项目的完整范本。目前已有2556人学习下载适合作为从Python语法过渡到App开发全流程的进阶案例。1. 为什么用 Python Kivy 写背单词 App背单词工具是典型的“逻辑不重、界面不炫、但生命周期很长”的移动应用。用 Kotlin 写原生自然没毛病可当你只想验证一套学习流程、维护自己的词库、每天在手机上过几十个单词的时候Python 生态里能直接产出 APK 的方案并不多Kivy 是其中能一条路走到 App 的那一个。“51斩百词”这个 Python 项目实战案例就是把 Python SQLite Kivy Virtualenv 串起来做背单词软件下载包里同时给出了 myapp-1.0.0-debug.apk、word.db 词库以及 PyCharm 专用的 KV 补全插件源码级别可复现装个模拟器就能跑起来。这个案例对 Python 入门后想找综合练习的人非常合适也适合准备交课程设计源码的在校生和想快速验证产品想法的独立开发者。2. 工程结构梳理与 Virtualenv 环境搭建2.1 下载包里每个文件是干什么的先对照项目文件把下载包里的内容过一遍新手最容易在这里翻车有人把 jar 包当代码工程有人把 APK 当成源码包还有人漏看使用说明文档直接开跑。下面这张表把文件名和实际用途对上了。文件作用使用说明myapp-1.0.0-debug.apkbuildozer 生成的调试安装包直接装到 Android 设备或模拟器word.dbSQLite 数据库存词库和学习进度程序第一次启动会把它复制到私有目录51斩百词项目使用说明.docx项目背景与运行说明建议先读里面有启动入口说明PyCharm_kv_completion.jar给 PyCharm 提供.kv文件补全的插件放到 PyCharm 的 lib 目录后重启back.jpg / c0c.jpg / raspberry.jpg界面背景图、卡片素材由.kv文件引用打包进 APK一个标准 Kivy 工程源码目录结构长这样51斩/ ├── main.py # 程序入口构建 App 和绑定事件 ├── ui/ │ ├── main.kv # Kivy 的 KV 语言界面描述 │ └── widgets.kv ├── data/ │ ├── word.db # SQLite 词库 │ ├── back.jpg │ └── c0c.jpg ├── requirements.txt └── buildozer.spec # 打包 Android APK 的配置文件main.py 决定程序入口.kv文件决定界面布局word.db 是数据层buildozer.spec 是打包配置。“Python 代码 KV 描述 SQLite 数据”是整个项目最核心的三段式结构。jar 插件属于开发辅助工具不属于运行链路但它的存在说明作者是在 PyCharm 里开发的如果你也用 PyCharm 写 Kivy建议把补全装上2.3 节会讲安装位置。2.2 Virtualenv 建环境与依赖锁定Kivy 的依赖链里有 Cython版本要求比较挑我一般会先建虚拟环境再装避免污染全局 Python。首先确保本机 Python 版本在 3.83.10 之间Python 安装完成后执行python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install --upgrade pip setuptools wheel pip install kivy2.1.0 pip install kivymd0.104.2 pip freeze requirements.txt这里把 Kivy 固定到 2.1.0 而不是直接pip install kivy原因是 Kivy 新版本对 Python 3.11 的 wheel 支持不稳定遇到找不到 wheel 或者编译报错时回退到 2.1.0 是社区里最常见的解决办法也是我实际拆过的项目里出现频率最高的版本。.venv是虚拟环境目录激活后pip freeze生成的 requirements.txt 要提交进工程别人 clone 下来执行pip install -r requirements.txt就能复现环境。KivyMD 是可选的 Material Design 组件库不装不影响核心背单词逻辑但能让按钮和列表的观感提升不少适合需要交课程设计的场景。装完依赖后写一个最小入口验证环境import kivy from kivy.app import App class WordApp(App): def build(self): return None if __name__ __main__: WordApp().run()这段代码能跑通说明 Kivy 环境没问题。如果在这里报AttributeError: module kivy has no attribute require多半是当前目录下有个叫kivy.py的文件和 Kivy 包重名了把脚本改个名就行。Python 环境的问题基本都在这一步解决之后再报错就是业务代码的问题了。2.3 PyCharm 的 KV 补全插件PyCharm 默认对.kv文件没有语法感知写ids:、on_release:全靠手打缩进错了就报错非常烦。下载包里那个 jar 是社区 kivy-completion 项目的编译产物安装方法很简单把PyCharm_kv_completion.jar复制到 PyCharm 安装目录的 lib 目录重启 PyCharm.kv文件就能获得基础补全和语法高亮。个别版本需要清一次缓存File - Invalidate Caches and Restart。装好后定义一个带 id 的 Widget在 Python 侧通过self.ids.xxx引用补全提示能直接帮你在 KV 文件和 Python 代码之间对齐名字少踩不少拼写坑。3. word.db 词库设计与查询接口3.1 单词表与进度表的结构背单词 App 的核心不在界面在数据。学习类 App 要回答两个问题词库从哪里来、学习状态怎么存。这个项目的 word.db 是 SQLite 单文件数据库Kivy 内置的 sqlite3 模块可以直接读写不需要额外服务。拿到项目先打开词库看表结构sqlite3 word.db .tables .schema words常见的表设计会把这些字段都建上这也是背单词项目最基础的落库方案CREATE TABLE IF NOT EXISTS words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL UNIQUE, phonetic TEXT, meaning TEXT, example TEXT, proficiency INTEGER DEFAULT 0, wrong_count INTEGER DEFAULT 0, right_count INTEGER DEFAULT 0, next_review TEXT -- ISO 格式日期2025-06-01 ); CREATE TABLE IF NOT EXISTS learn_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, word_id INTEGER, result INTEGER, -- 1 答对 0 答错 reviewed_at TEXT, FOREIGN KEY(word_id) REFERENCES words(id) );proficiency 是熟练度等级取值范围 05wrong_count 和 right_count 是累计统计next_review 控制复习日期。把词条状态和答题日志拆成两张表而不是全塞进 words是为了后面统计学习曲线时不拖慢主查询。很多背单词 App 后期越用越卡根因就是把所有行为记录写进一张宽表查询和更新互相阻塞。3.2 查询与更新的 Python 封装Kivy 里直接用 sqlite3 有一个多线程隐患界面线程和定时器线程同时写库会报database is locked。我一般会包一层短连接读写保证每次操作都拿到最新的连接和事务import sqlite3 import os DB_PATH os.path.join(os.path.dirname(__file__), data, word.db) def get_today_words(limit50): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row sql (SELECT * FROM words WHERE next_review date(now, localtime) ORDER BY proficiency ASC, wrong_count DESC LIMIT ?) rows conn.execute(sql, (limit,)).fetchall() conn.close() return [dict(r) for r in rows] def mark_word(word_id, correct): conn sqlite3.connect(DB_PATH) if correct: conn.execute( UPDATE words SET right_count right_count 1, proficiency MIN(proficiency 1, 5) WHERE id ?, (word_id,)) else: conn.execute( UPDATE words SET wrong_count wrong_count 1, proficiency MAX(proficiency - 1, 0) WHERE id ?, (word_id,)) conn.execute( INSERT INTO learn_log(word_id, result, reviewed_at) VALUES(?, ?, date(now,localtime)), (word_id, int(correct))) conn.commit() conn.close()date(now,localtime)取的是设备本地日期避免时区差导致今天该背的单词被next_review过滤掉。ORDER BY proficiency ASC, wrong_count DESC表示熟练度低的先出、错得多的先出这是最简单且有效的弱词优先策略不依赖额外算法。MIN(proficiency 1, 5)把熟练度上限封顶到 5防止一条 SQL 把单词刷爆。另一个细节是每次操作都conn.close()虽然频繁开关连接性能一般但换来了不会出现锁库的稳定体验。3.3 词库导入与 Android 私有目录word.db 是随 APK 一起打进去的静态资源但 Kivy 在 Android 上有个特性包内资源目录是只读的第一次运行必须把内置词库复制到应用私有目录否则程序能读词库但不能写学习进度from kivy.utils import platform import shutil import os def ensure_db(): if platform android: user_db os.path.join(os.environ[ANDROID_PRIVATE], word.db) if not os.path.exists(user_db): shutil.copy(DB_PATH, user_db) return user_db return DB_PATHos.environ[ANDROID_PRIVATE]是 Android 应用私有文件目录Kivy 打包后这个环境变量会自动注入。这段代码的作用是首次启动把包内只读词库复制到可写目录后续读写全部指向新的路径。这个坑是 Kivy 打包新手必踩的不处理的话桌面端跑得好好的上手机就报只读数据库错误。4. Kivy 界面与背单词交互循环4.1 用 ScreenManager 组织页面背单词 App 最少需要三个页面首页开始入口、背词页显示单词和释义、统计页查看进度。Kivy 的 ScreenManager 负责页面切换main.py 里注册好from kivy.uix.screenmanager import ScreenManager, Screen class HomeScreen(Screen): pass class ReviewScreen(Screen): pass class WordApp(App): def build(self): sm ScreenManager() sm.add_widget(HomeScreen(namehome)) sm.add_widget(ReviewScreen(namereview)) return sm配合 KV 语言写界面布局ReviewScreen: BoxLayout: orientation: vertical Label: id: word_label font_size: 34sp halign: center Button: text: 显示答案 on_release: root.show_answer() BoxLayout: size_hint_y: 0.3 Button: text: 认识 on_release: root.answer(True) Button: text: 不认识 on_release: root.answer(False)KV 语言的核心思路是用缩进描述层级关系Python 侧root.show_answer()必须在对应类里实现了否则运行时报 AttributeError。Label 的id: word_label可以像 CSS id 一样在 Python 代码里通过self.ids.word_label访问比 findViewById 直接。size_hint_y: 0.3 表示底部按钮组占父容器高度的 30%。4.2 背词主循环的实现背词主循环做四件事从词库取一组今天的单词、展示单词、点击“显示答案”展示音标和释义、根据用户点“认识”还是“不认识”更新数据库并滑到下一个词class ReviewScreen(Screen): def on_enter(self): self.words get_today_words(50) self.idx 0 self.show_word() def show_word(self): if self.idx len(self.words): self.ids.word_label.text 今日单词已完成 return self.current self.words[self.idx] self.ids.word_label.text self.current[word] def show_answer(self): w self.current self.ids.word_label.text f{w[word]} {w[phonetic]}\n{w[meaning]} def answer(self, correct): mark_word(self.current[id], correct) self.idx 1 self.show_word()on_enter是 Screen 的生命周期钩子每次进入页面都会重置单词列表正好匹配“每天进入就是新一轮背词”的节奏。mark_word更新熟练度后只把本地索引推进没有写状态文件如果想要 App 被系统杀掉后还能续背就要把self.idx在on_pause时写进 SQLite 或者配置文件这是 Kivy 应用生命周期里容易忽略的点。4.3 按钮事件与素材映射下载包里的 back.jpg、c0c.jpg、raspberry.jpg 除了当背景图更常见的用法是作为按钮背景或页面装饰。项目素材里的背景图可以直接用在 KV 中ScreenManager: HomeScreen: name: home BoxLayout: Image: source: data/back.jpg allow_stretch: True keep_ratio: False Button: text: 开始背词 size_hint: (0.6, 0.15) pos_hint: {center_x: 0.5, center_y: 0.3} on_release: root.manager.current reviewallow_stretch: True配合keep_ratio: False会让图片填满整个屏幕适合做封面背景。on_release: root.manager.current review是 KV 里实现页面跳转最直接的写法不用写 Python 回调。pos_hint和size_hint是 Kivy 布局的相对定位方式和 Android 的 dp 绝对值相比好处是不同屏幕尺寸下比例不变。5. buildozer 打包 APK 与真机调试5.1 初始化 buildozer.specKivy 打包 Android 的标准工具是 buildozer打包前先把环境装好。注意打包必须在 Linux 或 WSL 下进行Windows 原生不支持 buildozermacOS 只能打 iOS 或特殊渠道包pip install buildozer cython buildozer init用 buildozer init 生成的默认配置里需要修改的关键项如下[app] title 51斩百词 package.name fiftyone package.domain org.example source.dir . source.include_exts py,png,jpg,kv,ttf,db requirements python3,kivy2.1.0,kivymd0.104.2 orientation portrait fullscreen 0 android.permissions INTERNET.spec配置里最容易漏的是source.include_exts如果漏掉db后缀word.db 根本不会被打进 APK真机会直接报 FileNotFoundError。orientation portrait强制竖屏背单词场景几乎不需要横屏同时还能规避部分硬件的旋转崩溃。requirements把 Kivy 版本固定到 2.1.0和开发环境保持一致避免打包机和开发机版本差异带来的行为不一致。5.2 产出 Debug 包的完整流程配置好之后运行第一个打包命令buildozer -v android debug第一次构建要下载 Android SDK 和 NDK耗时通常在 15 到 60 分钟网络差的时候会卡在 downloads 阶段属于正常现象不是命令写错了。看到BUILD SUCCESSFUL后APK 会生成到bin/目录文件名格式类似下载包里的myapp-1.0.0-debug.apk。把手机连上 USB 并打开开发者调试模式可以直接执行buildozer android deploy run安装并启动 App。后缀-debug表示这是调试包包含调试符号适合后续排查问题。5.3 真机调试三板斧调试包运行时暴露的问题比反复打包更容易定位的是直接看日志buildozer android logcat | grep python adb logcat -s python第一行是 buildozer 封装好的过滤日志第二行是直接用 adb 配合 tag 过滤。Kivy 的异常信息在 Python 层面可能只显示一行 traceback但实际报错场景集中在三类sqlite3.OperationalError: attempt to write a readonly database没有处理 3.3 节的 ensure_db 复制逻辑FileNotFoundError: word.db.spec的source.include_exts里没加dbSDL surface相关崩溃orientation 没设 portrait旋转触发的绘制问题。这三条基本覆盖 Kivy 打包实战里八成以上的报错现场。建议在手机上把 App 完整走一遍“首页 - 背词 - 答对 - 杀进程 - 重开”流程重点看 on_enter 是否重复初始化列表、下次进入时是否还在原来的词上。6. 从能背到背得住记忆间隔与弱词优先逻辑当前 App 已经把每次答题结果写进了 learn_log但要让背单词真正有效率还要做两件事按记忆曲线排复习日期和按错误次数重排单词顺序。这里有一个完全不需要额外依赖的实现方案用 right_count 决定复习间隔用二项式递增策略控制 next_review。def next_interval(correct_count): base [1, 3, 7, 15, 30] level min(correct_count, len(base) - 1) return base[level]这个函数的含义是一个单词第一次答对明天复习连续答对第二次3 天后再来第三次 7 天第四次 15 天之后稳定在 30 天。len(base) - 1保证列表不越界核心思想是答对的次数越多复习周期拉得越长符合记忆衰减的基本规律。把它接到 mark_word 里在答对时把next_review更新为date(now, localtime, || next_interval(right_count) || days)答错则重置为今天的日期这套逻辑就可以马上跑进现有表结构里。第二个进阶改造是弱词优先策略的精细化。目前get_today_words用的是proficiency ASC, wrong_count DESC如果想精细化建议在 words 表加一个freq_rank字段记录词频等级从公开高频词表导入时给每个词标注“高中低频”然后在查询条件里加一条WHERE freq_rank 2000让用户先背高频 2000 词把有限的时间花在最高频的词汇上。动手做这个实验时可以先建一个 freq_rank 索引再用EXPLAIN QUERY PLAN看一下在 5000 词量级下是否走索引SQLite 在数据量不够大时全表扫描反而比索引快这也是这个 App 后续扩容时需要验证的一个点。把记忆间隔策略写进 mark_word数据库里累计的 right_count 就成了预测复习日期的唯一依据后续做学习统计图表时直接从 learn_log 聚合不需要再改表结构。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进