
简介这是一份基于安卓平台的记单词应用毕业设计项目源码主要面向计算机相关专业毕业生和安卓开发入门者核心围绕单词记忆功能包含单词浏览、自我测验、学习进度追踪等模块并接入本地语音合成能力适合用作课程设计、毕业设计或项目实战参考。压缩包共包含183个文件整体约17.55MB主要文件类型有界面布局文件、程序源码和图片资源同时配备动态库、数据库和构建脚本完整覆盖从界面搭建、业务处理到底层数据存储的开发结构目前已有96人浏览学习。通过这份源码可以学习基于SQLite的单词表设计、偏好设置存放学习进度、权限与内存管理等常用做法熟悉第三方语音库的接入步骤了解记忆曲线算法在安排复习计划时的实际应用整个项目结构清晰便于查找和修改对理解教育类安卓应用从零到实现的过程很有帮助。1. 记单词 App 作为 Android 毕业论文做什么、做到什么程度毕业论文选“记单词 App”属于多数院校都会收的方向业务边界清楚成果能直接装进手机演示数据库与算法两块都有内容可写。但这类选题有个隐蔽难点网上不少源码包只教你如何把单词塞进列表复习计划、错词反馈、进度可视化这些真正支撑“系统设计与实现”章节的部分反而没人展开讲。下面按一条能落地、能答辩的 Android 开发路线展开先搭工程并拆模块再把词库、复习计划、错词本建成 Room 表并导入 JSON 词库然后实现带间隔重复算法的背词流程最后处理打包发布、论文配图和几个高频坑。适合正在做毕业设计或课程设计的学生也适合想沉淀一份离线词库背词 App 源码的 Android 开发者。2. 先用 Android Studio 把记单词 App 工程立起来参数、依赖与目录2.1 创建工程时要选的参数与最小依赖集用 Android Studio 新建工程时语言选 Kotlin模板选 Empty Views Activity 就够了。模板带出的 MainActivity 先留着等背词页面能跑通再替换。compileSdk 用你本机 Android SDK Manager 里已装的那个版本targetSdk 与 compileSdk 保持一致minSdk 设为 24。这个组合能用到近几年的 API又不用为 Android 7.0 以下的文件访问和通知行为做额外兼容。数据层优先用 Room而不是直接写 SQLiteOpenHelper。Room 在编译期校验 SQL表名写错直接编译失败排查成本远低于运行时崩溃毕业论文的研究方法里也能明确写“引入 ORM 减少样板代码”这一条。Repository 用来隔离 DAO 和界面以后换成网络词典时不需要动背词页面。// 根目录 build.gradle.kts插件版本以 Android Studio 向导生成的值为准 plugins { id(com.android.application) version 8.2.2 apply false id(org.jetbrains.kotlin.android) version 1.9.22 apply false id(com.google.devtools.ksp) version 1.9.22-1.0.17 apply false }// app/build.gradle.kts plugins { id(com.android.application) id(org.jetbrains.kotlin.android) id(com.google.devtools.ksp) } android { namespace com.example.wordapp compileSdk 34 defaultConfig { applicationId com.example.wordapp minSdk 24 targetSdk 34 versionCode 1 versionName 1.0 } buildFeatures { viewBinding true } } dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) implementation(androidx.constraintlayout:constraintlayout:2.1.4) implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0) val roomVersion 2.6.1 implementation(androidx.room:room-runtime:$roomVersion) implementation(androidx.room:room-ktx:$roomVersion) ksp(androidx.room:room-compiler:$roomVersion) }代码里有两个点值得单独说明。viewBinding true开启视图绑定布局文件会生成对应的Binding类省掉一整套findViewById背词页里多处点击回调都能直接引用控件。ksp是 Room 的注解处理器版本必须和 Kotlin 版本匹配否则编译阶段会报找不到符号用 kapt 也能跑但构建时间更长毕设迭代期没必要浪费这个时间。提示版本号不要手抄网上的旧配置。Android Studio 创建向导生成什么组合就用什么组合Gradle、Kotlin、KSP 三者只要有一个不匹配就会在同步依赖时出现一长串报错。表 2-1 是这套依赖在论文里对应的章节写“系统设计”时可以直接引用依赖用途论文里对应的写点room-runtime、room-ktx本地持久化数据层设计与 E-R 图lifecycle-viewmodel-ktx状态管理MVVM 架构设计material统一控件样式用户界面设计2.2 目录结构data、repository、ui 三层分包毕设项目不建议拆多个 Gradle module。多模块会引入依赖传递、AndroidManifest 合并和构建时长问题论文的架构展示用包结构就足够证明分层。包结构按 data、repository、ui、util 划分其中 util 放间隔重复算法的纯函数不依赖 Android 的任何类后面写单元测试很方便。app/src/main/java/com/example/wordapp/ ├── MainActivity.kt # 容器承载底部导航 ├── data/ │ ├── entity/ # Room 实体类一张表一个类 │ ├── dao/ # 数据访问接口 │ └── AppDatabase.kt # 数据库实例与版本 ├── repository/ # 仓库层封装 DAO ├── ui/ │ ├── study/ # 背词主界面 │ ├── wordlist/ # 全部单词列表 │ ├── wrongbook/ # 错词本 │ └── settings/ # 字号、提醒开关 └── util/ └── SpacedRepetition.kt # 间隔重复算法纯函数划分逻辑与论文三层结构一一对应ui 表现层只处理点击和展示repository 负责转发数据data 层只做存取。答辩时如果被问“为什么这么分层”回答职责单一、ViewModel 持有背词状态比罗列设计模式名词更能让评委听明白。2.3 用 ViewModel 挂住背词状态旋转屏幕不丢进度背词页最怕转屏后回到第一张词卡。把当前词和今日进度放在 ViewModel 里界面因为旋转重建时数据不会被回收。下面是一个最小可用的 StudyViewModel依赖在构造时注入。class StudyViewModel(private val repository: WordRepository) : ViewModel() { private val _todayWords MutableLiveDataListWordEntity() val todayWords: LiveDataListWordEntity _todayWords private val _progress MutableLiveDataStudyProgress() val progress: LiveDataStudyProgress _progress fun loadTodayWords() { viewModelScope.launch { val list repository.getDueWords(System.currentTimeMillis()) _todayWords.value list _progress.value StudyProgress(list.size, 0) } } fun markReviewed(wordId: Long, result: StudyResult) { viewModelScope.launch { repository.markReviewed(wordId, result) _progress.value _progress.value?.let { it.copy(done it.done 1) } } } }MutableLiveData对外只暴露LiveData外部不能修改。loadTodayWords里用viewModelScope.launch调用挂起函数Room 的 suspend 查询会自动切到 IO 线程不需要手动传 dispatcher。Activity 需要在onCreate里调用一次loadTodayWords之后界面只管观察todayWords。这里有个容易写歪的地方repository 不要直接在 Activity 里new写在构造参数里方便写单元测试。等论文需要画类图时ViewModel、Repository、DAO 这条依赖链可以直接抄进“详细设计”一节。3. 记单词 App 的数据层Room 表设计与 JSON 词库导入3.1 单词、复习计划、错词本的表结构设计单词表是主表复习计划与单词一对一记录每次复习后的到期时间和间隔错词表单独建而不是只用一个难度字段是因为错词页需要立即列出历史错词而且还要统计错次数。单人用词库规模在几千词不需要分表索引按查询最多的字段加就可以。表 3-1 是三张表的核心字段表名关键字段说明wordsenglish, phonetic, meaning, example, example_cn, difficulty, is_favorite, group_name主表一次导入review_planword_id, due_time, interval_days, ease_factor, review_count复习状态与单词一对一wrong_wordsword_id, wrong_count, last_wrong_time答错一次记一次外键用CASCADE单词从词库删除时复习计划和错词记录一起删除避免留下孤儿数据。Room 里对应的实体如下Entity(tableName words) data class WordEntity( PrimaryKey(autoGenerate true) val id: Long 0, val english: String, val phonetic: String, val meaning: String, val example: String , val exampleCn: String , val difficulty: Int 1, val isFavorite: Boolean false, val groupName: String 默认 ) Entity( tableName review_plan, foreignKeys [ForeignKey( entity WordEntity::class, parentColumns [id], childColumns [wordId], onDelete ForeignKey.CASCADE )], indices [Index(wordId), Index(dueTime)] ) data class ReviewPlanEntity( PrimaryKey(autoGenerate true) val id: Long 0, val wordId: Long, val dueTime: Long, // 到期时间戳毫秒 val intervalDays: Int, // 当前间隔天 val easeFactor: Double, // 熟练度系数 val reviewCount: Int // 已复习次数 )dueTime用毫秒时间戳而不是Date是因为 SQLite 对时间字符串的排序规则依赖格式毫秒整数在查询和序列化时都不会出歧义。indices里同时给wordId和dueTime建索引原因在下一个小节讲。3.2 DAO 查询怎么写才能不拖慢背词页背词页首屏只取到期的词。查询条件直接写dueTime :now并按dueTime排序取前 N 条。给dueTime建索引是因为这是全 App 最热的查询条件没有索引时 SQLite 要全表扫描review_plan三千条数据可能感觉不出差别但到了界面频繁刷新时会掉帧。Dao interface WordDao { Query( SELECT * FROM words WHERE id IN ( SELECT wordId FROM review_plan WHERE dueTime :now ORDER BY dueTime ASC LIMIT :limit ) ) suspend fun getDueWords(now: Long, limit: Int): ListWordEntity Query(SELECT * FROM review_plan WHERE wordId :wordId) suspend fun getPlanByWord(wordId: Long): ReviewPlanEntity? Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertPlan(plan: ReviewPlanEntity) Query(UPDATE words SET isFavorite :favorite WHERE id :wordId) suspend fun setFavorite(wordId: Long, favorite: Boolean) }getDueWords的子查询先找出所有到期wordId再用IN回到单词表取详情。这样写的用意是让过期判断只扫review_plan的小表不碰words的大字段。getPlanByWord在背词页每次点击后都会调用作用是拿当前复习状态去计算下一次间隔必须命中wordId索引。Insert的冲突策略用REPLACE重复导入词库时不会因为主键冲突中断。注意 REPLACE 在 SQLite 里是删旧行再插新行主键序号会变如果你的 Room 版本较新可以直接用Upsert语义更清楚。3.3 从 assets 导入 JSON 词库的代码与参数词库不放进数据库文件而是随 APK 打包在assets/words.json。首启导入不仅演示方便论文里也能写“应用资源与用户数据分离”。JSON 格式控制在三个必填字段其余用 opt 方法给默认值[ { english: abandon, phonetic: /əˈbændən/, meaning: 放弃, example: Never abandon hope. } ]导入函数放在 repository 里由启动页调用一次避免在 Activity 里写文件解析class WordRepository( private val dao: WordDao, private val context: Context ) { suspend fun importFromAssets(assetPath: String words.json) { val json context.assets.open(assetPath).bufferedReader().use { it.readText() } val jsonArray JSONArray(json) val list ArrayListWordEntity(jsonArray.length()) for (i in 0 until jsonArray.length()) { val obj jsonArray.getJSONObject(i) list.add( WordEntity( english obj.getString(english).trim(), phonetic obj.optString(phonetic), meaning obj.getString(meaning), example obj.optString(example), exampleCn obj.optString(example_cn), groupName obj.optString(group, 默认) ) ) } dao.insertWords(list) } }函数里对english做了trim()这是导入时最值得注意的参数处理。单词字符串如果带首尾空格以后查重、排序、做索引都会出问题处理一次放在导入阶段成本最低。批量插入 Room 默认一个事务执行几千词不会有性能问题。导入后把words表行数显示在设置页截图放进论文就是一张很好的效果图。4. 记忆曲线与背词交互把间隔重复写进 Android 界面4.1 SM-2 的 Android 简化版间隔按天算的算法函数很多教程一上来就提艾宾浩斯 20 分钟、1 小时、12 小时的粒度但落地成 App 并不合适一天内反复提醒用户会直接被打扰演示时也看不到进度变化。常见做法是把间隔按天折算简化规则是答对把当前间隔乘熟练度系数答错间隔重置为 1 天。表 4-1 就是这个简化版的全部状态转移规则复习结果间隔变化熟练度系数变化记住当前间隔 × easeFactor不变忘记重置为 1 天减 0.2下限 1.3object SpacedRepetitionUtil { fun nextReview( currentPlan: ReviewPlanEntity?, result: StudyResult ): ReviewPlanEntity { val now System.currentTimeMillis() if (currentPlan null) { return ReviewPlanEntity( wordId 0L, dueTime now 24 * 60 * 60 * 1000L, intervalDays 1, easeFactor 2.5, reviewCount 1 ) } val newEase if (result StudyResult.FORGOT) { (currentPlan.easeFactor - 0.2).coerceAtLeast(1.3) } else { currentPlan.easeFactor } val newInterval if (result StudyResult.FORGOT) { 1 } else { (currentPlan.intervalDays * newEase).toInt() } return currentPlan.copy( intervalDays newInterval, easeFactor newEase, dueTime now newInterval * 24 * 60 * 60 * 1000L, reviewCount currentPlan.reviewCount 1 ) } }nextReview是纯函数不依赖 Android 任何类可以直接写 JUnit 测试。论文的实验章节里放一张间隔序列表比如第 1 天、第 3 天、第 7 天、第 14 天比贴大量源码更有说服力。easeFactor的初始值 2.5 来自 SM-2 原始实现下限 1.3 是为了防止系数掉到 1.0 以下后间隔永远不增长。4.2 背词卡片与 Android 进度条每次点击同步哪些状态背词主界面用 ViewPager2 放横向词卡顶部放 ProgressBar 表示今日任务完成比例。点击“记住”或“忘记”按钮时把结果交给 ViewModel 更新review_plan然后翻到下一张。卡片列表建议用 ListAdapter DiffUtil只更新变化的条目避免整页重绘。binding.btnRemember.setOnClickListener { val wordId adapter.currentWordId() viewModel.markReviewed(wordId, StudyResult.REMEMBER) binding.progressBar.progress viewModel.progressPercentage() viewPager2.setCurrentItem(viewPager2.currentItem 1, true) } binding.tvShowAnswer.setOnClickListener { binding.tvMeaning.visibility View.VISIBLE }ProgressBar 的max设为 100进度为done / total * 100。total 来自loadTodayWords返回的列表长度done 每次markReviewed后加一。这里一个常见错误是把progressBar.progress写在adapter.notifyDataSetChanged()之前更新视图还没刷新看到的数值总是少一。正确做法是先更新 ViewModel 里的 LiveData再在 observer 里改进度条。字号调节可以做成设置页的公共功能把字号存到 SharedPreferences背词页的 TextView 在onResume里重新setTextSize。这是论文设计里一个不起眼但答辩容易提的易用性需求实现起来只有几行代码前后端联动逻辑也很好讲。4.3 错词本与收藏演示脚本里最有说服力的两个入口答错时除了把间隔重置还要往wrong_words写一条记录或者累加wrong_count。答对三次才从错词本移出避免一次手滑导致单词永远留在错词本里。收藏则直接更新 words.isFavorite单词列表页用同一个字段过滤。Query( INSERT INTO wrong_words (word_id, wrong_count, last_wrong_time) VALUES (:wordId, 1, :now) ON CONFLICT(word_id) DO UPDATE SET wrong_count wrong_count 1, last_wrong_time :now ) suspend fun recordWrong(wordId: Long, now: Long)这条 SQL 用了ON CONFLICT做 upsert第一次答错插入后续答错累加计数不需要先查再改。演示时按这个顺序走背词页点“忘记”错词本立即出现该词在错词本里答对三次词条消失收藏一个单词在全部单词页过滤出来。这三步是答辩现场最直观的功能闭环截图顺序也可以按这个来。5. 毕业论文配图、APK 发布与每日复习提醒记单词 App 源码交付前的最后功课5.1 论文里放哪三张图类名必须与源码一致源码包交付前把下面三张图准备好系统架构图ui 表现层、repository 业务层、data 数据层、E-R 图words、review_plan、wrong_words 及其关系、背词时序图用户点击到 review_plan 更新的完整调用链。画图用 draw.io 就行重点是图里的类名和方法名必须与源码包一一对应评委按图找代码时找不到会直接怀疑工作量。5.2 签名打包 APK、发布演示版与导出词库数据库发布演示版 APK 的路径是 Build Generate Signed App Bundle or APK新建一个 keystore密码单独存一份之后每次更新都用同一个签名。开启 minify 后要保留数据层实体否则混淆后 Room 访问字段名会报错最简单的做法是在 proguard-rules.pro 中把 entity 和 DAO 的包名 keep 住。想导出演示数据用 adb 命令把数据库从调试包取出来adb exec-out run-as com.example.wordapp cat databases/worddb.db worddb.db这句命令里run-as只对 debug 包有效release 包没有这个权限。可以用它备份答辩前导好的演示词库也可以在不同模拟数据之间快速切换。5.3 用 WorkManager 每天 20:00 触发复习提醒的写法一个能写进论文“消息推送设计”的收尾功能是每天固定时间提醒复习。WorkManager 的周期任务比 AlarmManager 更省电设备重启后任务会自动恢复。下面在每天 20:00 触发一次提醒首次延迟设为距今晚 20:00 的毫秒差fun scheduleDailyReminder(context: Context) { val delay calculateDelayToNextTime(20, 0) val request PeriodicWorkRequestBuilderReviewReminderWorker(1, TimeUnit.DAYS) .setInitialDelay(delay, TimeUnit.MILLISECONDS) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( daily_review_reminder, ExistingPeriodicWorkPolicy.UPDATE, request ) }Worker 内部用 NotificationChannel 构建一条包含今日待复习数量的通知Android 8.0 以上不注册渠道通知就不会显示Android 13 以上还要动态申请 POST_NOTIFICATIONS 权限。用calculateDelayToNextTime计算从当前时间到下一个 20:00 的毫秒差值作为setInitialDelayWorkManager 会在满足条件后开始周期计时之后每天同一时间段触发。这是整个源码包里最值得单独截图讲解的启动流程。本文还有配套的精品资源点击获取