简介:一份基于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.apk | buildozer 生成的调试安装包 | 直接装到 Android 设备或模拟器 |
| word.db | SQLite 数据库,存词库和学习进度 | 程序第一次启动会把它复制到私有目录 |
| 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.8~3.10 之间,Python 安装完成后执行:
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install --upgrade pip setuptools wheel pip install kivy==2.1.0 pip install kivymd==0.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 是熟练度等级,取值范围 0~5;wrong_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(limit=50): 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(name='home')) sm.add_widget(ReviewScreen(name='review')) 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 = 'review'allow_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.spec
Kivy 打包 Android 的标准工具是 buildozer,打包前先把环境装好。注意打包必须在 Linux 或 WSL 下进行,Windows 原生不支持 buildozer,macOS 只能打 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,kivy==2.1.0,kivymd==0.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里没加db;SDL 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 聚合,不需要再改表结构。
本文还有配套的精品资源,点击获取