说起拿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实现插入排版
日记编辑器是整个项目里工作量最大的部分,它的目标是像备忘录一样,支持加粗、斜体、下划线,也能插入图片。我基于原生EditText+SpannableStringBuilder来做,不引入第三方大编辑器,原因后面避坑部分会说。
核心思想是:编辑器本质是一个带排版的文本容器,你用Spannable给某段文字加样式。比如加粗就是给选中范围设置StyleSpan:
SpannableStringBuilder 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里再把当前内容和光标位置写进Bundle,onRestoreInstanceState时优先恢复Bundle里的数据,其次才从数据库恢复草稿。这样即使进程被系统杀死,重新进入也基本能做到无缝恢复。
6. 交付与答辩:从演示Demo到让人信服的产品
6.1 答辩演示的动线设计
毕设答辩和平时自己跑Demo不一样,你不能闷头点来点去,得让评委跟着你的思路走。我设计了一条适合这个项目的演示动线,给你参考:
第一步,展示列表页和新建页面,快速写一篇带标题、正文、图片、心情、标签的日记,重点演示富文本排版和自动保存。
第二步,写完之后立刻杀进程重进,演示“草稿恢复”功能,顺势讲出状态保存和数据库设计。
第三步,搜索刚才输入的关键词,演示全文搜索和标签筛选;切到日历视图,点上面某一天,演示按日期浏览。
第四步,开夜间模式,再切回来,讲主题资源配置。最后做一次导出备份,展示生成的备份文件。
整个动线大概8分钟,能把项目的技术覆盖面完整展现出来,评委想打断都找不到空洞。
6.2 几个可能会被问到的问题,提前想好答案
答辩时老师问的问题,往往就是从你的项目细节里挑的。我整理了几个高频问题以及应对思路:
- “你的数据存在哪里?如果用户卸载了怎么办?”:这个问题考察的是存储方案和数据生命周期。回答时要说明SQLite + 私有目录存储的优势,再补充导出备份机制,让用户能在卸载前保留数据。
- “富文本编辑器的排版格式是什么?”:答HTML序列化,能解释清楚
Spanned.toHtml()和Html.fromHtml()的转换关系,就能证明你是自己实现过逻辑,而不是单纯调了个第三方库。 - “图片为什么会压缩?不压缩行不行?”:回答内容围绕OOM风险、存储占用、加载速度三点展开,顺带讲清楚采样压缩和内存计算方式,直接体现你踩过坑、理解了原理。
- “如何应对不同分辨率屏幕?”:把自适应UI、dp/sp单位、缩略图尺寸生成逻辑讲清楚,再举一两个测试机的适配结果,这个问题就能漂亮地过。
6.3 这个项目还能怎么扩展
如果你还有余力,这个日记本项目后续可以朝几个方向继续做:一是加情绪分析与周报,用本地算法统计近一周的心情词汇,生成一周日记回顾;二是补充指纹解锁的加密机制,把数据库文件用密钥加密后落盘,这个方向很适合深造;三是做一个简单的时间轴地图,按日记的地点信息在地图上展示足迹,配合定位权限实现。以上这些都是可落地的扩展方案,开发量和难度循序渐进,哪怕答辩前只完成其中一个,项目的完整性都会上一个台阶。
写在最后的体会
做完这个基于Android的个人日记本项目,我最深的体会是:毕业设计里真正宝贵的不是那几个页面和几行代码,而是你被迫把“产品需求”“技术实现”“异常处理”“数据安全”串起来想一遍的整个过程。如果你也选了或准备选这个题目,别把它当成一个“练手小Demo”,而是把自己当成一个真的要上架应用商店的开发者,用那种标准要求自己。你会发现问题会变多,但收获也会随之成倍增加。
最后再分享一个实操小技巧:在开发过程中,我会在每个功能完成时用真机跑一遍“杀进程重进”的测试,这个操作虽然简单,但能揪出很多界面状态和数据持久化的问题。提前养成习惯,答辩时你会少很多尴尬。