
简介这份移动开发期末大作业的备忘录应用项目包面向计算机相关专业学生、课程设计人员及移动开发入门者可作为期末大作业或实战练习的完整参考。项目经导师指导并获评九十八分所有源码均在本地编译通过并严格调试确保能够稳定运行。压缩包共六十七个文件大小约四点零七兆字节内含二十二个xml界面布局、十个java核心逻辑、十个webp预览图及若干jpg/png项目截图另附gradle构建配置、properties参数、apk安装包和Android导入步骤及问题解决办法文档目录结构清晰便于快速查阅与工程导入。附带的导入文档与截图能帮助还原项目环境减少配置排错时间适合需要高质量模板的学生直接使用。当前已有一百五十人学习下载综合评价较高很适合作为期末大作业或课程设计的参考资源。1. 一个备忘录App凭什么拿高分先看懂评分点在交付物上期末周接到“移动开发期末大作业-备忘录app”时第一反应往往是“这题太简单了”。但反过来想这个题目恰恰是移动应用开发课程里性价比最高的一道题它同时覆盖界面搭建、数据存取、增删改查、生命周期和状态切换任何一个环节都能做出区分度。项目名里“含导入文档和项目截图”不是附件要求而是三个独立评分点——工程能不能在别人电脑上跑起来、截图有没有展示关键交互、导入文档能不能帮评委一次打开项目这三点甚至比功能本身更影响分数。这篇笔记把一条能直接照做的路径拆开讲技术选型怎么定不翻车代码按什么顺序写数据表怎么设计以及演示时怎么避免当场卡壳。适合用 Android Studio 做原生开发的读者也适合想过一次通过、还想冲高分的同学。2. 技术选型与项目骨架原生Android路线下两Activity分包最省心2.1 先看评分表再定技术栈为什么原生Android最稳期末大作业的技术栈选择优先考虑的不是“哪个框架新”而是“老师按什么标准给分”。大多数院校的移动应用开发课纲以 Android 原生 Java/Kotlin 为主线作业验收时老师会直接打开工程看代码结构。这种情况下选 Flutter 或 React Native 等于给自己增加两道额外的坎一是课纲里没讲代码很难自圆其说二是这两套工具链的依赖体积更大导入到老师电脑上同步失败的风险更高。如果你是按移动应用开发技能大赛那种风格准备的用 Kotlin 写确实更有说服力但期末节点还是以课程评分表为准。三种路线的取舍可以这样看技术栈课程贴合度排错难度演示信任感适合场景Android 原生 Java/Kotlin高课纲内容直接覆盖低报错信息网上全高评委能看懂每一步绝大多数期末大作业Flutter / React Native中课纲一般不涉及高遇到环境问题耗时中容易被追问原理做过完整跨平台项目的学生网页套壳 / 低代码拖拽低明显偏离课程低但一问就露馅低基本无法答辩不推荐我的建议很直接用原生 Java 写。Java 在期末阶段比 Kotlin 更保守——Kotlin 的协程、扩展函数如果写不好答辩时反而被问住Java 代码啰嗦但每一行都能解释清楚。技术栈选保守不是能力问题是风险控制问题。2.2 项目骨架两个Activity加一个工具类够用到答辩很多同学一上来就建三个 Activity列表、编辑、详情。但详情页面在备忘录场景里完全可以复用编辑页——详情就是在只读模式下的编辑页。少一个 Activity 就少一套布局、少一条 Intent 传参路径、少一个生命周期坑。所以我一般会用两 Activity 结构MainActivity负责列表和搜索EditActivity负责新增和编辑数据通过 Intent 的 extra 传递。MemoApp/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/memo/ │ │ │ ├── model/MemoBean.java │ │ │ ├── db/DBHelper.java │ │ │ ├── db/MemoDao.java │ │ │ ├── adapter/MemoAdapter.java │ │ │ ├── ui/MainActivity.java │ │ │ ├── ui/EditActivity.java │ │ │ └── util/TimeUtil.java │ │ ├── res/layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_edit.xml │ │ │ └── item_memo.xml │ │ └── AndroidManifest.xml │ └── build.gradle ├── 导入文档.md ├── 项目截图/ └── build.gradle这个分包思路背后是简单的分层model只放数据载体db里DBHelper管建库建表、MemoDao管增删改查语句adapter只负责把数据绑到列表项上util放时间格式化这类公共方法。答辩时老师问“你的项目怎么组织的”照这个结构说就能讲清楚职责边界。项目截图/和导入文档.md放在工程根目录而不是塞进src里。这样老师打开工程文件第一眼就能看到交付物不用钻进代码里找。截图放四到五张关键界面就够主页列表、编辑页、搜索效果、置顶和删除后的状态。2.3 能让老师直接导入的工程Gradle、SDK和导入文档“能导入”是大作业的生命线。工程在你机器上编译通过不算数要在老师电脑上也能打开才算。最常见的翻车原因是 Gradle 版本、SDK 版本和依赖仓库不一致。我建议 build.gradle 里的版本号按你本机 Android Studio 能跑通的写不要盲目追新android { namespace com.example.memo compileSdk 33 defaultConfig { applicationId com.example.memo minSdk 24 targetSdk 33 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.google.android.material:material:1.9.0 }注意两点。第一compileSdk和targetSdk保持一致最稳当前用的是 33 的话模拟器和老师的测试机一般都在覆盖范围内。第二依赖尽量精简不引入 Room、Navigation 这类重组件——期末项目用不上反而把同步时间拉长。如果 Gradle 同步卡住检查镜像仓库配置或切换离线模式导入文档里要把这一步写清楚。“导入文档.md”不是项目说明书而是“后悔药文档”。第一段写环境要求第二段写“编译失败先查这三处”JDK 版本、SDK 路径、依赖仓库是否可达。第三段才是功能简介。写的时候把自己当成一个刚拿到工程的人照着文档走一遍看能不能一口气编译过。3. 核心功能落地的关键代码增删改查、搜索和提醒代码怎么拆3.1 首页列表当老师第一眼看到的是RecyclerView主界面用 RecyclerView 展示备忘录列表这是移动端列表的标准做法。不要用 ListView——虽然 ListView 也能跑但 RecyclerView 的复用机制和 ViewHolder 模式是答辩时的高频问题主动用上等于给自己铺好一个“被问住但能答上来”的点。public class MemoAdapter extends RecyclerView.AdapterMemoAdapter.VH { private ListMemoBean data new ArrayList(); private OnItemClickListener listener; public interface OnItemClickListener { void onClick(int position); } public void setData(ListMemoBean data) { this.data data null ? new ArrayList() : data; notifyDataSetChanged(); } Override public void onBindViewHolder(NonNull VH holder, int position) { MemoBean bean data.get(position); holder.title.setText(bean.getTitle()); holder.time.setText(TimeUtil.format(bean.getUpdateTime())); if (bean.isPinned() 1) { holder.pinIcon.setVisibility(View.VISIBLE); } else { holder.pinIcon.setVisibility(View.GONE); } holder.itemView.setOnClickListener(v - listener.onClick(position)); } Override public int getItemCount() { return data.size(); } }setData里的判空处理很容易被忽略——数据库查询结果为 null 时直接传入会让getItemCount空指针演示时当场崩溃。notifyDataSetChanged()在这里足够用不需要上 DiffUtil因为备忘录数据量撑死几十条全量刷新成本可以忽略。答辩时如果有人问这一点回答“数据量小全量刷新简单可靠”反而是加分项。列表页还需要一个空状态。getItemCount() 0时显示“还没有备忘录点右下角新建”否则隐藏。这个细节看起来小却是截图里最出效果的一屏因为空状态能证明你真的处理过边界情况。3.2 新增和编辑共用一个EditActivity传id是-1还是真实idEditActivity接收 Intent 里的memo_id字段-1表示新增其他值表示编辑。这样一套布局服务两个场景保存逻辑里用memoId区分走插入还是更新private void saveMemo() { String title etTitle.getText().toString().trim(); String content etContent.getText().toString().trim(); long now System.currentTimeMillis(); long remindTime remindSwitch.isChecked() ? remindCal.getTimeInMillis() : -1L; MemoBean bean new MemoBean(); bean.setTitle(title); bean.setContent(content); bean.setUpdateTime(now); bean.setRemindTime(remindTime); if (memoId -1) { bean.setCreateTime(now); memoDao.insert(bean); } else { bean.setId(memoId); memoDao.update(bean); } setResult(RESULT_OK); finish(); }保存前要做一道校验标题为空时用内容的前 10 个字当标题内容是空则提示“内容不能为空”。这个大作业场景里用户经常只写内容不写标题如果存库直接放过列表页就会显示一条没有标题的灰条看起来像 bug其实是数据问题。预设默认标题这个处理能让演示顺畅很多。回传结果用RESULT_OK主界面在onActivityResult里重新查库刷新列表。不要直接传一个“保存成功”的布尔值因为主界面本来就要刷新数据直接重新查最简单。3.3 搜索功能别用contains:SQL通配符和防注入要一起写搜索框在主界面顶部监听输入变化后调MemoDao的查询方法。关键词匹配标题和内容按置顶优先、更新时间倒序排列public ListMemoBean search(String keyword) { SQLiteDatabase db getReadableDatabase(); String sql SELECT * FROM memo WHERE title LIKE ? OR content LIKE ? ORDER BY is_pinned DESC, update_time DESC; String kw % keyword %; Cursor cursor db.rawQuery(sql, new String[]{kw, kw}); ListMemoBean result new ArrayList(); while (cursor.moveToNext()) { // 从cursor逐列读出字段封装成MemoBean加入result } cursor.close(); return result; }LIKE的模糊匹配用了?占位符这是防 SQL 注入的底线写法。很多教程直接写LIKE % keyword %单次跑没问题但答辩时老师问“如果 keyword 里带单引号会怎样”答不上来就是硬伤。用rawQuery传参的方式一句话就能解释清楚。搜索结果的排序和列表页保持完全一致都是is_pinned优先、update_time倒序。这样从搜索切回列表时视觉顺序不变不会让评委觉得是两个页面。3.4 提醒功能做“应用内提醒”最稳避开Android 12精确闹钟限制系统级提醒要接AlarmManager加SCHEDULE_EXACT_ALARM权限还要处理 Doze 模式、应用被杀后广播收不到等一串问题。期末这个时间点用系统推送做提醒的风险很高真机上演示时闹钟不响的概率不低。我常用的方案是“应用内未办提醒条”打开 App 时顶部横幅提示“你有 2 条过期待办”。private void checkRemindStatus(ListMemoBean list) { long now System.currentTimeMillis(); for (MemoBean bean : list) { if (bean.getRemindTime() 0 bean.getRemindTime() now bean.getStatus() 0) { bean.setStatus(2); // 2表示已过期未办 } } }这段代码放在查询数据之后、adapter.setData之前状态直接写回数据列表。status的三态设计要提前定好0 待办、1 已完成、2 已过期。列表项“完成”按钮把 status 改成 1数据库同步更新首页把 status 为 2 的条目淡色展示形成视觉区分。如果你一定要做系统通知有一个折中办法把系统推送做成加分项而不是核心项代码写在独立工具类里导入文档里注明“真机上需手动授予通知权限”演示时用真机。但默认验收路径还是走应用内提醒稳得多。4. 数据持久化SQLite表结构设计决定能不能拿高分4.1 三种存储方式对比期末选手写SQLite别选Room数据存储是备忘录 App 的技术核心也是最容易被答辩追问的地方。三种方案的取舍直接关系到你能不能守住建表、查询、更新这些基础问题存储方案学习成本答辩友好度翻车概率适合场景SharedPreferences极低低存不了复杂结构低仅保存设置项手写 SQLite中SQL 是课纲重点高每一行都能讲中代码量大但透明期末大作业首选Room高注解和依赖多中讲不清原理容易露馅高依赖同步慢有把握的进阶项目Room 是 Google 官方推荐的 ORM但对期末来说成本偏高要加 kapt 插件配注解处理器要写 Entity、Dao、Database 三套接口导入时多三个依赖就可能少三份稳定性。手写SQLiteOpenHelper的好处是所有逻辑摆在明面上建表写在onCreate里查询写在MemoDao里评委问“这个 SQL 是什么意思”你能直接答。SharedPreferences 只适合存“是否已引导”“主题色偏好”这类单项配置。用 JSON 串去存整个列表属于歪路因为一旦数据量大每次读出来都要整体反序列化性能和代码复杂度都会失控。4.2 表结构一张memo表八个字段够应付所有功能很多同学建表只建三列id、title、content。跑通时觉得挺顺但一到答辩就露怯因为支撑不了搜索、置顶、提醒和完成状态。我用的表结构可以一次把这几个功能全兜住:CREATE TABLE memo ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL DEFAULT , content TEXT NOT NULL DEFAULT , remind_time INTEGER NOT NULL DEFAULT -1, is_pinned INTEGER NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL ); CREATE INDEX idx_memo_update ON memo(update_time DESC);字段设计看着简单每个字段都有讲究id用AUTOINCREMENT保证删除后新增的条目拿不到旧主键。如果删掉第 2 条再新增没有 AUTOINCREMENT 的话新条目可能复用 id2出现详情页错位的诡异问题。remind_time存 epoch 毫秒整数默认 -1 表示未设提醒。用整数而不是TEXT保存时间是为了直接算差值判断是否过期。is_pinned默认 01 表示置顶排序时一个ORDER BY is_pinned DESC就解决。status配合 3.4 的三态0 待办、1 完成、2 过期。create_time和update_time拆开存新增时两个都写编辑时只更新update_time。这在“今天”“最近三天”这类筛选场景里会用到。idx_memo_update索引是为了让按时间排序的查询不至于全表扫描。数据量小的时候感知不到但这个索引的存在能让答辩时多一个可讲的点。建索引的 SQL 可以写在onCreate里一起执行。4.3 升级数据库的正确姿势别用DROP TABLE应付答辩SQLiteOpenHelper的onUpgrade是期末项目里最容易被糊弄过去的方法。很多人图省事直接在onUpgrade里DROP TABLE再重建这样老用户升级后数据全没。答辩现场只要老师问一句“用户升级后数据还在吗”这个方案就崩了。正确的做法是版本号递增每个版本只执行对应的一段迁移Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE memo ADD COLUMN remind_time INTEGER NOT NULL DEFAULT -1); } if (oldVersion 3) { db.execSQL(CREATE INDEX idx_memo_update ON memo(update_time DESC)); } if (oldVersion 4) { db.execSQL(ALTER TABLE memo ADD COLUMN status INTEGER NOT NULL DEFAULT 0); } }这段代码的写法是“按版本号递进”不是“按目标版本覆盖”。oldVersion 2表示从任何低于 2 的版本升上来都要执行加列操作这样从 1 直接升到 4 时三段都会执行。用ALTER TABLE加列可以保留已有行数据这是迁移的基本原则。导入文档里值得专门写一小节“数据库版本设计”把版本 1 到版本 4 的变更列成清单。这个内容很多大作业不会写写了就是区分度。4.4 导出备份一个把列表写成txt的小工具导出功能是给“数据持久化”章节画龙点睛的。它不复杂但能让评委看到你考虑了数据可迁移性。做法是把备忘录列表写成一个 txt 文件File dir getExternalFilesDir(backup); File file new File(dir, memo_backup_ System.currentTimeMillis() .txt); try (BufferedWriter writer new BufferedWriter(new FileWriter(file))) { for (MemoBean bean : list) { writer.write(bean.getTitle() | bean.getContent() | TimeUtil.format(bean.getUpdateTime())); writer.newLine(); } Toast.makeText(this, 已导出到 file.getAbsolutePath(), Toast.LENGTH_LONG).show(); } catch (IOException e) { e.printStackTrace(); }关键点落在getExternalFilesDir(backup)上。这个方法拿到的是 App 专属外部目录既不需要WRITE_EXTERNAL_STORAGE权限也天然绕开 Android 10 以后的分区存储限制。如果换成Environment.getExternalStorageDirectory()直接写公共目录Android 11 以上大概率直接报权限错误这也是一处常见翻车点。导出后吐司里打印完整路径方便演示时找出文件。5. 备忘录App避坑清单5个高频翻车点还原验收现场5.1 导入工程后一编译就报错Gradle同步了一半就失败现象老师拿到工程导入 Android Studio界面一直转圈最后提示Unable to resolve dependency或SDK location not found。原因依赖仓库访问慢导致超时或者目标机器的 SDK 版本和你的工程不一致。很多同学自己机器上开着代理没感觉换台机器就原形毕露。解决工程里不要依赖冷门仓库只保留 Google 和 Maven Central 的默认配置。导入文档第一行就写清楚“JDK 1.8/17、compileSdk 33、minSdk 24”老师机器版本不匹配时能立刻知道调哪里。另外打包一份 Gradle 离线缓存不现实最稳的办法是文档里写三步排查顺序先看 SDK 路径再看 JDK 版本最后开离线模式重试。5.2 列表刷新但界面没变化notifyDataSetChanged没执行现象在新增页保存后返回主页列表还是老样子重启应用才看到新数据。原因onActivityResult里没写刷新逻辑或者setData没触发notifyDataSetChanged。我在 3.1 里专门强调过setData内部要调notify但很多人把刷新写在了 Activity 的onResume里结果setData又没调用到。解决onActivityResult收到RESULT_OK后重新查库一次再调adapter.setData(newList)。另外把onResume里的刷新代码去掉避免重复加载。这个坑的根源是“数据变更后没有主动通知适配器”养成“改数据必 notify”的习惯能根治。5.3 中文内容在模拟器上展示成乱码或输入法弹不出现象演示时用模拟器想在搜索框里输入中文结果输入法弹不出来或者打出来的字变成问号老师以为你编码没做对。原因Android Studio 模拟器默认英文输入法部分版本的 Google 拼音输入法没有预装。解决最稳妥的方案是演示走真机其次是预置一条包含中文的演示数据让搜索可以直接点“最近”标签跳到结果不用现场打字。我一般在MainActivity的onCreate里加一个if (dao.count() 0) 插入3条示例数据既解决空列表问题又保证搜索和详情有内容可看。5.4 删除后新增的数据ID对不上点详情进错条目现象先删除第 2 条备忘录再新增一条然后点新增那条的详情弹出的却是旧数据的内容。原因列表点击事件里把position当成 id 传给了详情页。删除后列表的position发生位移UI 位置和数据库主键不再对应。解决点击事件里必须传bean.getId()holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onClick(bean.getId()); } });接口回调签名改成onClick(int memoId)EditActivity拿到的才是真正的主键。这条属于数据一致性的大坑代码审查和答辩时都会被重点追问最好在写的时候一步到位。5.5 导出txt后找不到文件答辩现场翻车现象演示导出功能Toast 提示导出成功但老师问“文件在哪”你打开文件管理器找不到路径。原因getExternalFilesDir路径在Android/data/包名/files/下很多文件管理器默认不显示这个目录容易让人以为导出失败了。解决演示时不要用文件管理器找文件。两个思路一是代码里把完整路径 Toast 出来现场用adb shell ls /sdcard/Android/data/com.example.memo/files/backup/展示二是加一个“分享”按钮调系统的Intent.ACTION_SEND把文件直接发到邮件或备忘录。后者在真机演示时效果更直观评委能看到文件真真实实地被发出去。6. 验收前最后一道工序演示脚本、边界测试和两个提分细节6.1 演示脚本三分钟把评分表走一遍评委看演示的时间通常不超过三分钟。我会按这条顺序来冷启动进主页看空状态和列表点新建加一条带时间的备忘录用关键词搜出刚才那条置顶它改一个字段删除另一条最后重启应用确认数据还在。整个流程覆盖增删改查、搜索、置顶、持久化六个评分点中间不切换页面超过两次。新增操作一定要现场做不要点预置数据糊弄预置数据是给搜索用的不是给新增用的。6.2 边界输入测试清单输入场景预期行为标题为空只有内容用内容前 10 个字做标题正常保存标题和内容都为空格拦截提示“内容不能为空”不写入数据库标题超过 50 个字符布局不崩列表项单行省略显示提醒时间设为过去的时刻打开 App 后进入过期态status2连续新增 50 条列表滚动流畅不卡顿、不错位6.3 截图和导入文档的提分细节截图不是把手机桌面拍一张就完事。我会截四张关键界面列表空状态、列表非空状态、编辑页带键盘、搜索后的结果页。每张图上用红框标出关键按钮或状态这种对比式截图能让评委不用装 App 就能看出功能全貌。导入文档的最后一页放一份功能自测表每项对应一个截图这本质上是把整理“移动应用开发笔记”的习惯用在了作业上。带过几次大作业后我养成一个习惯功能写完先当评委问自己一句如果这是别人电脑上第一次打开跑不跑得起来。这个习惯救过我很多次。希望帮到你。本文还有配套的精品资源点击获取