
说起拿Android做个人日记本这个毕业设计选题我一开始心里是有点打鼓的。市面上的日记App一抓一大把功能花哨的也很多一个学生做出来的东西凭什么有亮点但这个项目真正做完之后我反而觉得“移动端电子日记本”是最能体现Android原生开发基本功、也最容易做出完整度的方向之一。它不像电商或社交项目那样重业务逻辑却能把你对四大组件、数据库、文件存储、多媒体处理、UI自定义这些能力的理解全部串起来是一个很典型的“小而全”项目。这篇文章我会从选题动机、技术选型、数据库设计、功能模块落地、界面适配一直聊到调试踩坑和交付答辩尽量把我实际操作中的思考过程也写进去。如果你正在准备类似的毕业设计或者想做一个自己的日记类App练手这里面大部分经验应该都能直接抄作业。1. 为什么选个人日记本当毕业设计从需求倒推技术选型1.1 先拆需求别急着写代码很多同学做毕设有个通病拿到一个题目就开始建工程、写界面做到一半发现功能之间互相打架再回头改设计。我的做法是先把自己当成目标用户把日记App的“真实使用场景”一条一条列出来。日记本这个工具表面上就是“写文字 保存 翻看”但往细了想需求其实是分层的基础层新增日记、编辑日记、删除日记、按时间倒序查看列表。这是任何日记App的底线缺了它产品就不成立。体验层写日记的时候能插入图片、记录天气和心情、给日记打标签能够按关键词搜索按月份翻看历史数据要能备份不能一卸载就全没了。安全感日记属于私密内容要有一个锁至少是本机密码或指纹锁不然别人拿起手机就能翻。管理感要有统计信息比如这个月写了多少篇、连续记录了多少天让人有持续写下去的动力。把这一层需求拆出来后整个项目的模块边界就清楚了日记的增删改查 附件管理 标签分类 搜索归档 锁定与备份。后面的所有技术选型其实都是在为这几件事服务。1.2 Java还是Kotlin、原生还是跨平台我为什么这么选说实话现在新项目用Kotlin已经是主流了但毕业设计这个场景里Java并没有被淘汰。我选Java主要出于两个考虑第一Java相关的参考资料和案例实在太多。做毕设期间你会频繁遇到问题搜索“Android Studio Java xxx”能看到的教程、报错解决方案比Kotlin多了不止一个量级。对于时间紧张、没有太多项目经验的学生来说把精力花在业务实现而不是语言特性上是更务实的选择。第二很多学校的教学大纲还是以Java为核心答辩老师看代码的熟悉程度更高。你用一个大家都能看懂的语音在答辩时解释逻辑是更有利的老师不需要先适应语法再理解你的思路。至于为什么不选跨平台框架——原因也很直接。题目要求就是基于Android原生而且像日记的排版编辑、图片压缩、日历控件这些功能原生生态里能直接找到成熟方案跨平台框架在这些细节上往往要绕弯子处理不好还会出现体验割裂的问题。对一个需要“独立完成”的项目来说原生方案最不容易翻车。2. 本地优先的存储方案数据库表结构与数据安全演进2.1 为什么不做云端同步把本地方案做到极致当时我也纠结过要不要接入云端同步毕竟现在主流日记App都有这个功能。但仔细评估后我决定放掉这个需求原因有两个一方面云端同步牵扯的东西太多了要选后端方案、要做登录注册、要处理多端数据合并工作量和坑都会翻好几倍一个毕设项目很难在有限时间内做透。另一方面日记数据的私密性很强用户是否愿意把日记传到云端本身就是个问题。我反而觉得把“本地存储 导出备份”做好才是更符合日记场景的产品定位。所以最终方案是数据全部本地存储提供两种安全锁图案锁和指纹锁再提供一个“导出全部日记”功能把数据打包成文件转到用户自己的其他设备上。这样既规避了后端开发的巨大工作量又给了用户数据自由。2.2 表结构设计日记、图片、标签关系拆解数据库是这种工具类App的核心表的划分直接影响后面代码好不好写。我用的本地数据方案是SQLite通过Android自带的SQLiteOpenHelper管理。核心表设计如下日记主表CREATE TABLE tbl_diary ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, mood TEXT, weather TEXT, create_time TEXT, update_time TEXT, is_favorite INTEGER DEFAULT 0, is_delete INTEGER DEFAULT 0 );这里我特意增加了is_delete字段而不是直接物理删除。日记这种产品误删是致命问题所以我做了“回收站”机制删除时先标记为1在回收站里停留7天用户点彻底删除才真正从数据库移除。这个设计在答辩时是个很好的加分点因为体现了产品思维。图片附件表CREATE TABLE tbl_image ( id INTEGER PRIMARY KEY AUTOINCREMENT, diary_id INTEGER, image_path TEXT, sort_order INTEGER );图片和日记是一对多关系拆成单独的表是为了在列表页显示缩略图时能快速按diary_id查询不需要去解析主表里的一长串文本。sort_order字段用来控制图片在详情页的显示顺序这个字段在后面实现拖拽排序时特别有用。标签表与关联表CREATE TABLE tbl_tag ( id INTEGER PRIMARY KEY AUTOINCREMENT, tag_name TEXT UNIQUE ); CREATE TABLE tbl_diary_tag ( diary_id INTEGER, tag_id INTEGER );我之前有一个方案是把标签直接存成字符串用逗号分隔。后来发现如果要做“按标签筛选”字符串匹配的效率和数据一致性都很差比如把标签改名就得遍历所有日记改文本。所以用了经典的三表关联设计UNIQUE约束保证同一个标签不会重复插入。2.3 数据库升级的实际操作这个点很多教程不会提但我强烈建议毕业设计里也做一下。比如我第二版加了一个“置顶”字段如果直接用旧版本App打开新数据库就会崩。正确做法是在SQLiteOpenHelper.onUpgrade()里做版本判断Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE tbl_diary ADD COLUMN is_top INTEGER DEFAULT 0); } }这保证了升级的时候不会把用户已有的日记数据清掉。答辩时老师如果问你“用户升级App后数据怎么办”你能直接答出这一段印象分会高不少。3. 核心功能落地日记编辑、多媒体嵌入与检索模块的实现3.1 富文本编辑器用原生EditText实现插入排版日记编辑器是整个项目里工作量最大的部分它的目标是像备忘录一样支持加粗、斜体、下划线也能插入图片。我基于原生EditTextSpannableStringBuilder来做不引入第三方大编辑器原因后面避坑部分会说。核心思想是编辑器本质是一个带排版的文本容器你用Spannable给某段文字加样式。比如加粗就是给选中范围设置StyleSpanSpannableStringBuilder builder new SpannableStringBuilder(editText.getText()); int start editText.getSelectionStart(); int end editText.getSelectionEnd(); builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); editText.setText(builder);插入图片时我会把图片复制到App私有目录再在文本里插入一个ImageSpan。这里有一个很重要的坑ImageSpan默认会占一行如果不想让图片把整个行撑得很难看要设置ImageSpan.ALIGN_BASELINE同时要控制插入图片的默认宽度不超过屏幕宽度的85%。富文本保存时我用了自定义方案Spanned.toHtml()转成HTML字符串存到数据库的content字段读取时Html.fromHtml()还原。优点在于数据体积小、检索依然方便缺点在于原生的toHtml()对一些自定义Span支持不完美所以我在恢复时做了兜底转不了的Span就丢样式只留文本确保不崩溃。3.2 图片选择和压缩不压缩就不叫移动端开发在手机上处理图片第一个硬道理就是“别用原始分辨率”。现在的手机随便拍张照片都是4~8MB不压缩直接存存储卡、直接加载到内存轻则卡顿重则直接OOM崩溃。我的压缩策略分两步。第一步是采样压缩读取图片时先解析边界信息算出一个合适的缩放比BitmapFactory.Options opts new BitmapFactory.Options(); opts.inJustDecodeBounds true; BitmapFactory.decodeFile(path, opts); int sampleSize calculateInSampleSize(opts, 1080, 1920); opts.inSampleSize sampleSize; opts.inJustDecodeBounds false; Bitmap bitmap BitmapFactory.decodeFile(path, opts);第二步是质量压缩把图片写回保存到私有目录FileOutputStream out new FileOutputStream(savePath); bitmap.compress(Bitmap.CompressFormat.JPEG, 85, out);这样一张原始8MB的照片最终只有200~300KB显示在手机上完全够用。列表页我会再生成一个宽200像素的缩略图这样滑动日记列表的时候加载速度才会快内存压力也会小很多。3.3 全文搜索与日历视图让日记能快速被找到日记写得多了之后找内容就是核心痛点。我用了两种方式组合。第一种是SQLite自带的LIKE模糊查询也就是标题和内容包含关键字就返回结果String sql SELECT * FROM tbl_diary WHERE is_delete 0 AND (title LIKE ? OR content LIKE ?);如果数据量上万条仅靠LIKE性能会下降但个人日记的数据量一般到不了这个级别所以这个方案简单可靠不用引入全文索引。第二种是日历视图。我用了一个可横向滑动的自定义日历条点击某一天就能查看当天写了多少篇日记。实现方式是遍历数据库的create_time字段用SimpleDateFormat格式化到“yyyy-MM-dd”精度后和所选日期比对。这里有个容易踩的坑是时区问题必须统一用Calendar.getInstance()来构造日期否则在某些设备上会差一天。3.4 草稿箱与自动保存写日记时最让人安心的功能有人会觉得草稿功能很鸡肋其实不然。我自己测试的时候就遇到过写了几百字切出去回个消息回来发现App被系统回收内容全部丢失。所以我在编辑器里做了防抖自动保存每次输入停顿3秒后自动把当前内容和编辑位置存到内存和数据库草稿字段里重新进入那篇日记时把草稿恢复出来并弹一个提示“已恢复未保存的草稿”。实现方式很直接给EditText加一个TextWatcher在afterTextChanged里设置一个Runnable延时执行文本继续变化就取消前一个任务重新计时模拟防抖。这一块的代码量不大但对体验的提升是质的飞跃。4. 界面适配与Android原生交互让App“有点样子”4.1 主界面设计从丑到顺手的迭代日记类App的主界面一般就是两种形态卡片列表流和按时间线排列。我选的是卡片列表 悬浮新增按钮这基本是目前日记产品的标准交互范式。第一版我直接用了原生RecyclerView套一个默认的LinearLayoutManager一屏只能看到两三条日记信息密度太低了。后面我换成了GridLayoutManager用“瀑布流”效果让图片多的日记以大图卡片展示没有图片的就以文字卡片展示。瀑布流在这个场景下有个天然好处用户刷列表的时候看到的是图文混合的信息视觉节奏比干巴巴的文字列表舒服得多。列表项加载缩略图我用的是原生方式加载前先判断缓存里有没有没有再从本地文件解码。翻页和快速滑动时会频繁调用onBindViewHolder如果每次都走磁盘IO帧率会掉所以我把图片解码做了内存缓存量级控制在20MB以内防止把内存打爆。4.2 主题与夜间模式一开始我觉得夜间模式是锦上添花但实际做下来它的工程意义并不小。原理很老套在values和values-night两套资源目录里分别定义颜色和主题样式App通过AppCompatDelegate.setDefaultNightMode()切换。不过我踩了一个坑直接在Activity里切换模式后界面闪烁和状态错乱经常出现。正确做法是切换后立刻调用recreate()并确保所有界面颜色都是从主题资源里取的而不是写死颜色值。如果有一两个界面用了写死的#FFFFFF夜间模式下白底就会特别刺眼。项目做完你会意识到“可换主题”不是某种高级技巧而是对资源管理能力的一次全面体检。4.3 运行时权限申请从API 23开始的必修课日记App要读写图片和保存备份文件自然要处理权限问题。Android 6.0之后的运行时权限机制最常被新手忽略。我一开始直接在AndroidManifest.xml里声明读写权限结果跑到Android 11以后的测试机上一调用相册就崩溃。后来我写了一个统一的权限申请工具类哪些页面使用哪些权限集中在进入页面前申请用户拒绝时给出说明弹窗并再次引导。需要注意的一点是如果你的targetSdk是30及以上还想用本地相册的话其实最好用系统自带的PhotoPicker它不需要申请任何存储权限。我把“选图”从“读SD卡”换成了ActivityResultContracts.PickVisualMedia代码更简洁权限问题也彻底绕开了。这部分的思路也建议你用到其他涉及文件的选择场景。5. 必须知道的坑性能、兼容性与数据安全实战5.1 图片加载导致的OOM崩溃排查过程项目做到中后期我遇到了一个非常典型的崩溃在真机上浏览日记详情页连续翻看几篇带大图的日记就闪退日志显示OutOfMemoryError。定位过程是这样的先看崩溃堆栈发现崩在BitmapFactory.decodeStream于是怀疑是图片内存占用过高。然后我检查了详情页加载Bitmap的代码发现当时为了省事直接用了原始的图片文件路径去解码完全没压缩。一张1920x1080的图片在ARGB_8888格式下就是1920x1080x4字节大约8MB内存而系统给每个应用堆内存一般只有128MB到256MB翻个十几篇日记加上页面本身的开销自然就崩了。修复办法就是前面提到的采样压缩 质量压缩并且详情页只加载“适配当前屏幕分辨率”的图片尺寸不加载原始大图。这个问题的排查思路其实很通用看到OOM先别慌先确认是哪一步在吃内存再用采样压缩和缓存去解决。5.2 时区与日期格式化为什么你的日记晚了一天有一天我在测试时发现晚上快到零点时写的日记保存后列表显示成了第二天甚至会出现“在未发布的未来”这种荒谬结果。原因在于我用SimpleDateFormat格式化时间时取了系统默认时区而部分测试机时区设置不对格式化出来的字符串和保存时刻差了8小时。后来我把所有时间存储统一改成System.currentTimeMillis()毫秒值。展示层需要“yyyy-MM-dd HH:mm”时才用Calendar.getInstance()转成本地时间再格式化。这样无论是哪个时区的用户显示的都一定是本地时间。以后凡是涉及时间存储的功能都可以直接沿用这个思路存储用时间戳展示才做格式化。5.3 数据备份与导出给用户一条“后悔之路”日记数据最怕的就是误删或者换手机没法迁移。我做了两个层面的备份方案。在App内我用SQLite数据库定期拷贝 附件目录打包的方式生成一个带时间戳的压缩包存到“导出备份”目录。这样用户卸载前可以先导一份重装后App检测到备份包可以一键还原。同时在单篇日记详情页我提供“分享/导出本篇”功能可以把单篇日记的内容和图片打包成文本或图片分享出去。这个功能看似简单但在答辩演示时效果特别好因为它把“手机端个人随笔记录与管理系统”从单纯的存储工具延伸到了内容再分发的场景评委一看就知道你考虑过真实用户需求。5.4 自定义Dialog和Fragment的状态恢复另一个大坑是编辑器页面的状态丢失。用户正在写日记突然来个电话App被系统杀掉重启后发现输入内容没了。我之前只做了数据库层面的自动保存但忽略了Activity被重建时EditText中的状态恢复逻辑。完整的处理方式是双保险平时输入通过“防抖保存”把草稿存进数据库在onSaveInstanceState里再把当前内容和光标位置写进BundleonRestoreInstanceState时优先恢复Bundle里的数据其次才从数据库恢复草稿。这样即使进程被系统杀死重新进入也基本能做到无缝恢复。6. 交付与答辩从演示Demo到让人信服的产品6.1 答辩演示的动线设计毕设答辩和平时自己跑Demo不一样你不能闷头点来点去得让评委跟着你的思路走。我设计了一条适合这个项目的演示动线给你参考第一步展示列表页和新建页面快速写一篇带标题、正文、图片、心情、标签的日记重点演示富文本排版和自动保存。第二步写完之后立刻杀进程重进演示“草稿恢复”功能顺势讲出状态保存和数据库设计。第三步搜索刚才输入的关键词演示全文搜索和标签筛选切到日历视图点上面某一天演示按日期浏览。第四步开夜间模式再切回来讲主题资源配置。最后做一次导出备份展示生成的备份文件。整个动线大概8分钟能把项目的技术覆盖面完整展现出来评委想打断都找不到空洞。6.2 几个可能会被问到的问题提前想好答案答辩时老师问的问题往往就是从你的项目细节里挑的。我整理了几个高频问题以及应对思路“你的数据存在哪里如果用户卸载了怎么办”这个问题考察的是存储方案和数据生命周期。回答时要说明SQLite 私有目录存储的优势再补充导出备份机制让用户能在卸载前保留数据。“富文本编辑器的排版格式是什么”答HTML序列化能解释清楚Spanned.toHtml()和Html.fromHtml()的转换关系就能证明你是自己实现过逻辑而不是单纯调了个第三方库。“图片为什么会压缩不压缩行不行”回答内容围绕OOM风险、存储占用、加载速度三点展开顺带讲清楚采样压缩和内存计算方式直接体现你踩过坑、理解了原理。“如何应对不同分辨率屏幕”把自适应UI、dp/sp单位、缩略图尺寸生成逻辑讲清楚再举一两个测试机的适配结果这个问题就能漂亮地过。6.3 这个项目还能怎么扩展如果你还有余力这个日记本项目后续可以朝几个方向继续做一是加情绪分析与周报用本地算法统计近一周的心情词汇生成一周日记回顾二是补充指纹解锁的加密机制把数据库文件用密钥加密后落盘这个方向很适合深造三是做一个简单的时间轴地图按日记的地点信息在地图上展示足迹配合定位权限实现。以上这些都是可落地的扩展方案开发量和难度循序渐进哪怕答辩前只完成其中一个项目的完整性都会上一个台阶。写在最后的体会做完这个基于Android的个人日记本项目我最深的体会是毕业设计里真正宝贵的不是那几个页面和几行代码而是你被迫把“产品需求”“技术实现”“异常处理”“数据安全”串起来想一遍的整个过程。如果你也选了或准备选这个题目别把它当成一个“练手小Demo”而是把自己当成一个真的要上架应用商店的开发者用那种标准要求自己。你会发现问题会变多但收获也会随之成倍增加。最后再分享一个实操小技巧在开发过程中我会在每个功能完成时用真机跑一遍“杀进程重进”的测试这个操作虽然简单但能揪出很多界面状态和数据持久化的问题。提前养成习惯答辩时你会少很多尴尬。