最近帮一些准备毕业设计的朋友看选题,发现“基于Android的电子日记本”出现频率相当高。这个题目看起来简单,实际做起来并不轻松,牵扯到数据存储、界面交互、本地资源管理、隐私保护等一整套移动端开发的基础能力。我手上正好有一套完整的实现思路和踩坑记录,趁这次机会梳理出来,给正在做类似项目或者打算复现这套东西的同学一个参考。
这套系统本质上是一个“移动端个人随笔记录与管理系统”,核心是解决用户在手机上随时记录、分类整理、后续检索的完整闭环。虽然叫“日记本”,但功能上已经超出了简单记事本的范畴,更像是一个轻量级的带私有特性的内容管理工具。文章会覆盖从技术选型、数据库设计、功能模块拆解,到实际编码和问题排查的全过程,所有代码和方案都是我在真实设备上验证过的,可以直接抄作业。
1. 项目整体设计与技术选型
1.1 客户端技术栈选择
项目名里明确写了Java,那客户端主语言就用Java,Android Studio作为集成开发环境,最低支持版本建议定在Android 8.0(API 26)以上。这个区间覆盖了绝大多数在用设备,同时又不需要处理太多老版本的分屏、权限兼容问题。
界面方面我用的是传统的XML布局加RecyclerView组合,没有引太多的第三方UI库。原因很直接:毕业设计类项目,稳定压倒一切,第三方库版本一旦和compileSdkVersion不匹配,编译报错能折腾人半天。核心就用系统自带的Material Components库,做出来的界面不会太寒碜,又不引入多余的依赖。
存储方面,我用了Room数据库。它是官方在SQLite之上封装的ORM框架,编译期就能校验SQL正确性,运行时还能返回LiveData做响应式更新。对日记本这类数据结构不算复杂、但查询场景比较丰富的项目,Room比手写SQLiteOpenHelper省下大量重复代码,也降低了出错概率。
1.2 数据存储方案选型对比
电子日记本的数据有一个特点:写入频繁、单条数据体积不大、但总量会持续增长。在选存储方案时,我认真对比了几种常见做法:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SharedPreferences | 实现简单,键值对读写 | 不适合大量结构数据,查询能力弱 | 保存用户设置、登录态 |
| 普通文件存储 | 灵活,可自由组织内容 | 检索困难,并发写入有风险 | 保存富文本原文、导出文件 |
| SQLite原生 | 关系型数据,SQL能力强 | 样板代码多,易写错SQL | 小型工具数据 |
| Room | SQLite官方封装,结构清晰 | 有一定的学习成本 | 中大型结构化数据,首选 |
| 云端数据库 | 多端同步 | 依赖网络,域名部署复杂 | 本地上传型需求,本项目不采用 |
实测下来,日记列表、标签、图片路径、用户配置这几类数据,用Room管理最顺手。日记正文如果是富文本或长文,单独导出一份文件备份也是合理的,但检索和列表还是要靠数据库。
1.3 架构模式与包结构规划
很多同学一上来就写Activity,所有逻辑全堆在一起,到后期加功能根本动不了。这次我采用了轻量级的MVVM模式,不引入DataBinding,只用ViewModel加LiveData。
包结构大致是这样的:
com.example.diary ├── data │ ├── db // Room数据库、DAO、实体 │ ├── repository // 数据仓库,统一对上层提供接口 │ └── model // 数据模型 ├── ui │ ├── main // 主界面、列表 │ ├── editor // 编辑页面 │ ├── search // 搜索页面 │ └── settings // 设置页 ├── viewmodel // ViewModel层 ├── util // 工具类,时间处理、加密、图片压缩 └── adapter // RecyclerView Adapter这样分层虽然前期写的文件多一些,但后期扩展功能(加个回收站、加个多选删除)非常轻松。我在实际开发中按照这个结构,新增一个功能模块基本不用动别的层的代码。
2. 数据库设计与核心表结构
2.1 数据表设计与字段规划
日记本的核心表就是日记表。我设计的表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | LONG | 主键,自增 |
| title | TEXT | 标题,可空 |
| content | TEXT | 正文内容 |
| weather | TEXT | 天气,可空 |
| mood | TEXT | 心情,可空 |
| tags | TEXT | 标签,JSON数组字符串 |
| image_paths | TEXT | 图片路径,JSON数组 |
| create_time | INTEGER | 创建时间(毫秒时间戳) |
| update_time | INTEGER | 修改时间(毫秒时间戳) |
| is_delete | INTEGER | 软删除标记,0正常,1回收站 |
| is_draft | INTEGER | 草稿标记 |
一个比较重要的经验:时间字段用INTEGER存毫秒时间戳,不要用TEXT存"2025-01-01 12:00:00"这种格式。毫秒时间戳在排序、比较、按时间范围查询时效率极高,展示时再通过SimpleDateFormat转成需要的格式就行。
@Entity(tableName = "diary") data class Diary( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String? = "", val content: String = "", val weather: String? = "", val mood: String? = "", val tags: String? = "[]", val imagePaths: String? = "[]", val createTime: Long = System.currentTimeMillis(), val updateTime: Long = System.currentTimeMillis(), val isDelete: Int = 0, val isDraft: Int = 0 )2.2 标签与图片的存储策略
标签和图片是我再三纠结过的地方。很多人习惯单独建一张标签表和一张图片表,用外键关联日记主表。但在实际项目中,这种设计的复杂度远超收益。
我的方案是:标签直接用JSON数组字符串存在diary表里,图片路径同理。比如tags字段存["生活","工作","旅行"],imagePaths存["file:///storage/emulated/0/xxx/20250101.jpg"]。
为什么这么做?第一,日记与标签之间本质上是弱关联,不需要频繁做多表join查询。第二,存储路径字符串比存二进制数据更灵活,数据库里存的是文件路径,真正的图片文件放在应用私有目录下。第三,要按标签筛选时,SQL的LIKE查询完全够用,日记数量到几千、几万条也不会有明显性能问题。
图片也是如此。绝对不要用BLOB把图片二进制塞进数据库,数据库体积会迅速膨胀,备份、迁移都是灾难。正确做法是把图片文件存到应用私有目录(getExternalFilesDir),数据库只保留路径字符串。
2.3 DAO层的查询设计
Room的DAO层是整个数据层的核心。我设计了几组关键查询方法:
- 分页查询日记列表:按updateTime倒序排列,每页20条
- 按标题或内容模糊搜索:用LIKE关键字
- 按标签筛选:用JSON包含匹配
- 软删除与恢复
- 按时间范围统计日记数量
@Dao public interface DiaryDao { @Query("SELECT * FROM diary WHERE isDelete = 0 ORDER BY updateTime DESC LIMIT :limit OFFSET :offset") List<Diary> getDiaryList(int limit, int offset); @Query("SELECT * FROM diary WHERE isDelete = 0 AND (title LIKE '%' || :keyword || '%' OR content LIKE '%' || :keyword || '%')") List<Diary> searchDiaries(String keyword); @Query("SELECT * FROM diary WHERE isDelete = 0 AND tags LIKE '%' || :tag || '%'") List<Diary> getDiariesByTag(String tag); @Query("UPDATE diary SET isDelete = 1 WHERE id = :id") void softDelete(long id); @Query("SELECT COUNT(*) FROM diary WHERE isDelete = 0 AND createTime BETWEEN :start AND :end") int countByDate(long start, long end); }LIKE查询在数据量几万条以内性能问题不大,但如果后面真做全文检索,可以考虑引入SQLite的FTS4全文搜索。这个属于进阶优化,基础版不需要。
2.4 数据库版本升级策略
用Room最不能忽略的就是版本管理。很多同学开发时直接改实体类然后卸载重装App,这在测试阶段问题不大,但如果已经发布的App,用户数据会全部丢失。
我的做法是:Room的版本号从1开始,每次表结构变更(新增字段、新增表)版本号加1,同时写Migration迁移逻辑:
static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE diary ADD COLUMN weather TEXT"); database.execSQL("ALTER TABLE diary ADD COLUMN mood TEXT"); } };这个迁移逻辑在没有用户数据需要保留的原型阶段可以省,但到了联调、演示阶段千万不能省。否则演示到一半,数据库升级直接闪退,整个项目瞬间变成大型社死现场。
3. 核心功能模块的实现细节
3.1 日记主列表与分页加载
列表界面是用户看到的第一屏,直接决定项目给人的第一印象。我用 RecyclerView + LinearLayoutManager 实现。
要点在于分页加载。日记数量几百条以后,一次性全部加载出来会卡顿,图片资源也会吃紧。我用了一个比较朴素的分页策略:每页20条,监听RecyclerView滑动到最底部时自动加载下一页。
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); if (lastVisibleItemPosition >= layoutManager.getItemCount() - 3) { loadMore(); // 加载下一页 } } });列表项的布局我做了几套ItemType:纯文字型、带图片型、草稿型。三种类型用不同布局渲染,代码中通过判断isDraft和imagePaths是否为空来分发。
卡片式布局会是这里首选视觉风格。每张卡片显示标题、摘要(正文前80字截断)、日期、天气和心情的小icon。图片在卡片上有两种处理:第一张图作为封面,或者九宫格预览。我建议第一版做封面图就够了,九宫格对列表加载性能要求很高,收尾图处理不当容易OOM。
列表Adapter还有一个细节:摘要截断时,如果正文含有换行符或图片占位符,要过滤后再截取。我踩过一次坑,图片路径字符串被当成正文截进摘要里,列表页显示出一串乱码路径,很影响观感。
3.2 日记编辑器的制作
编辑器是整个项目中功能最密集、代码量最大的模块。我用的基础控件是EditText,在此基础上做了一整套增强。
第一是自动保存。日记本最怕写了一半退出,内容全丢。我在编辑页面开了一个Handler循环,每30秒自动保存当前内容到草稿。同时Activity的onPause()生命周期回调里也强制保存一次。这样能有效避免数据丢失。
private final Handler autoSaveHandler = new Handler(Looper.getMainLooper()); private final Runnable autoSaveRunnable = new Runnable() { @Override public void run() { saveDraft(); autoSaveHandler.postDelayed(this, 30000); } };第二是富文本支持。如果我在这部分只做纯文本,那么“日记本”看起来会太简单。我在编辑器里加入了加粗、斜体、下划线、标题字号四种格式化能力,用EditText的SpannableStringBuilder实现。
private void applyStyle(int style) { int start = editText.getSelectionStart(); int end = editText.getSelectionEnd(); if (start >= end) return; SpannableStringBuilder builder = new SpannableStringBuilder(editText.getText()); switch (style) { case STYLE_BOLD: builder.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case STYLE_ITALIC: builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; } editText.setText(builder); editText.setSelection(end); }第三是图片插入。编辑器中插入图片用的是系统相册选择,跳转一个ImagePickerActivity,多选后压缩再拷贝到应用私有目录,拿到path把图片以ImageSpan的形式插入到EditText当前光标位置。图片压缩用的是BitmapFactory的inSampleSize配合ExifInterface处理旋转,在适配不同厂商相机产出的大尺寸照片时非常关键。
3.3 日历、提醒与时间管理
日记本有一个很打动答辩老师的功能点:日历视图。我用RecyclerView实现了一个月历,格子右上角显示红点表示当天有日记。点击天数跳到当天的日记列表,或者新建日记时默认填上当天时间。
提醒功能上,我用AlarmManager实现了一个“每日随笔”定时提醒。Android 12以上需要精确闹钟权限,Android 13以上需要POST_NOTIFICATIONS通知权限。这在处理时需要小心,真正写入权限申请逻辑,否则定时提醒在部分机型上根本不触发。
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); Intent intent = new Intent(this, DiaryRemindReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerTime, intervalMillis, pendingIntent);我给提醒功能做了一个开关,默认关闭,只有用户在设置页主动开启才会注册闹钟。这样既满足功能演示需要,又避免在测试机上天天弹通知烦到卸载App。
3.4 搜索、标签筛选与回收站
搜索是一个性价比很高的功能,写着不复杂,但演示效果很好。搜索页我做了两项:标题/内容关键字搜索,和标签筛选。两种模式共用一个搜索框,输入关键词后,先查匹配结果,再根据结果中涉及的标签自动生成标签筛选Chip。
回收站这个模块强烈建议加上。核心是软删除思路:日记表里的isDelete字段标记为1,列表页和搜索页默认只查isDelete=0的数据。回收站页面查询isDelete=1的数据,提供恢复和彻底删除两个操作。
@Query("UPDATE diary SET isDelete = 0 WHERE id = :id") void restoreDiary(long id); @Query("DELETE FROM diary WHERE id = :id") void permanentlyDelete(long id);回收站里一般还会加一个“清空回收站”按钮,一键清除全部已删除数据。有了这个模块,项目的完整度就一下子上去了,答辩展示时也能多讲一个场景。
3.5 隐私保护与数据导出
日记属于敏感数据,隐私保护是绕不开的话题。我的方案是应用锁:App进入前台时,检查安全设定的开关状态,如果开启则展示一个锁屏界面,输入PIN码或验证指纹后才能进入。
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); if (keyguardManager.isKeyguardSecure()) { Intent authIntent = keyguardManager.createConfirmDeviceCredentialIntent("验证身份", "请输入锁屏密码"); startActivityForResult(authIntent, REQUEST_CODE_CONFIRM_CREDENTIAL); }这个方案的优点是代码量少、安全性足够、无需引入第三方指纹SDK。它在演示时非常加分,毕竟现在大家对自己的隐私都很注意。
数据导出我做了两个出口:一是单篇日记分享为文本或图片,用系统ShareSheet把内容发给微信、备忘录等App;二是全部数据导出为HTML文件,用户可拷贝到电脑上备份。HTML导出是加分项,代码也不难,就是拼接字符串。
4. 核心功能实现中的关键代码解析
4.1 Repository仓库层封装
数据仓库层是连接ViewModel和Dao的桥梁,所有数据库操作都从这里进出。它的好处是:上层界面不需要关心数据从哪来,也不关心是本地还是网络。如果后续拓展云备份功能,只需要改Repository内部实现,UI层不用动。
public class DiaryRepository { private final DiaryDao diaryDao; private final ExecutorService executor = Executors.newFixedThreadPool(3); public DiaryRepository(Application application) { DiaryDatabase db = DiaryDatabase.getInstance(application); diaryDao = db.diaryDao(); } public LiveData<List<Diary>> getDiaryListLiveData() { return diaryDao.getDiaryListLiveData(); // 返回LiveData,自动更新UI } public void insertDiary(Diary diary) { executor.execute(() -> diaryDao.insert(diary)); } public void updateDiary(Diary diary) { executor.execute(() -> diaryDao.update(diary)); } public void softDelete(long id) { executor.execute(() -> diaryDao.softDelete(id)); } }所有数据库操作都放到线程池执行,避免阻塞主线程。LiveData的封装让列表页能够自动感知数据变化,写完一篇日记回到列表页,新日记立刻出现在顶部,不需要手动刷新。
4.2 图片压缩与媒体处理
图片处理是移动端日常日记本的一个难点。现在的手机拍照动不动就是4000万像素,一张原图10MB以上,如果直接加载到内存,爆掉是分分钟的事。我封装了一个ImageCompressor工具类:
public static String compressImage(String sourcePath, String destDir) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(sourcePath, options); int inSampleSize = 1; int maxWidth = 1280; while (options.outWidth / inSampleSize > maxWidth) { inSampleSize *= 2; } options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(sourcePath, options); // 处理旋转问题 int rotation = getRotation(sourcePath); if (rotation != 0) { Matrix matrix = new Matrix(); matrix.postRotate(rotation); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } File destFile = new File(destDir, UUID.randomUUID().toString() + ".jpg"); try { FileOutputStream fos = new FileOutputStream(destFile); bitmap.compress(Bitmap.CompressFormat.JPEG, 85, fos); fos.flush(); fos.close(); } catch (IOException e) { e.printStackTrace(); } if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } return destFile.getAbsolutePath(); }在拍摄照片插入日记的场景中,这个工具类用起来效果很好。压缩到1280px宽度、85%质量,一张图控制在300KB左右,列表加载、存储、备份都舒服。
4.3 日记导出为HTML的实现
导出HTML的代码逻辑:查询所有未删除日记,拼接HTML头部和样式,遍历日记内容生成正文区块,每篇日记加上标题、时间、内容、图片标签,最后写文件。
public void exportToHtml(String destPath) { List<Diary> diaryList = diaryDao.getAllDiaries(); StringBuilder html = new StringBuilder(); html.append("<!DOCTYPE html><html><head><meta charset='UTF-8'>"); html.append("<style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:16px}"); html.append(".diary{border-bottom:1px solid #eee;padding:16px 0}</style></head><body>"); for (Diary diary : diaryList) { if (diary.isDelete == 1) continue; html.append("<div class='diary'>"); html.append("<h2>").append(escapeHtml(diary.title)).append("</h2>"); Date date = new Date(diary.createTime); html.append("<p>").append(new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA).format(date)).append("</p>"); html.append("<div>").append(escapeHtml(diary.content)).append("</div>"); if (diary.imagePaths != null && !diary.imagePaths.isEmpty()) { // 遍历图片路径,生成相对路径的img标签 // 同时把图片文件复制到导出目录 } html.append("</div>"); } html.append("</body></html>"); // 写入文件 }注意转义问题,正文中如果用户输入的HTML代码或特殊字符,直接用会导致格式错乱甚至安全隐患。专门写了一个escapeHtml方法处理。
5. 常见问题与排查技巧实录
5.1 列表图片加载卡顿和OOM
这是做带图日记本最常遇到的问题。很多人用系统自带的setImageResource加载图片,但日记本里的图片是动态的、体积较大的文件,这种操作方式轻则卡顿,重则直接触发OOM崩溃。
我的解决思路是把图片加载功能统一交给Glide框架处理。Glide在图片缩放、内存缓存、磁盘缓存上已经非常成熟,几行代码就能完成图标的加载与占位处理:
Glide.with(context) .load(diary.getFirstImagePath()) .placeholder(R.drawable.placeholder) .error(R.drawable.error_image) .centerCrop() .into(imageView);严格禁止在自定义Adapter的getView/onBindViewHolder里用BitmapFactory.decodeFile直接加载大图。我看到不少同学就是这么干的,结果列表稍微往下滑几百条,内存直接报警。
5.2 数据库升级导致崩溃
一个很经典的坑:开发阶段数据库版本是1,改了实体类,Run一下正常。过几天又改了实体类,卸载App重装,正常。但某次用旧版本升级到新版本时,直接崩。
报错信息大概是这样的:
java.lang.IllegalStateException: A migration from 1 to 2 was necessary but has not been found.解决方法是:要么正式发布前保留好Migration,要么就是把debug阶段的数据库清掉。我给所有用到Room的项目都加了一个策略——在开发过程中如果只是原型验证阶段,每次改字段就卸载重装;一旦进入功能完善阶段,严格写Migration并升级版本号。
5.3 EditText输入法与布局被遮挡
编辑页面在某些机型上,弹出软键盘后,整个输入区域被顶上去,并且光标不可见。原因在于没有适配windowSoftInputMode。
我建议在AndroidManifest.xml的editor Activity配置:
<activity android:name=".ui.editor.EditorActivity" android:windowSoftInputMode="adjustResize" />同时布局根节点用ScrollView或者NestedScrollView包裹EditText,保证内容可以滚动到输入法可见区域。这段配置在实际设备上适配站在不同的手机系统上都能稳定工作。
5.4 日期格式和时区问题
时间戳如果直接用System.currentTimeMillis(),在不同的时区设置下,显示出来的日期可能差一天。如果用户用手机设置改了时区,而日记只记录了一个时间戳,那跨时区展示会出现混乱。
我处理的思路是在展示时使用用户本地时区,存储时保留毫秒时间戳。Java的SimpleDateFormat默认使用系统时区,直接格式化时间戳就正确地展示了本地时间。但如果用户有跨时区需求,就需要考虑存储UTC时间戳,展示时再转换。
示例代码:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA); sdf.setTimeZone(TimeZone.getDefault()); String displayTime = sdf.format(new Date(createTime));5.5 分享功能在部分机型无效
分享日记功能,用Intent.ACTION_SEND调起系统分享面板,但在部分国产ROM上分享面板空白或者分享出去的不是预期内容。
排查下来主要是两个原因:一是没有传对MIME类型,二是没有给分享的临时文件配置FileProvider。我把日记导出为txt或图片文件后,通过FileProvider生成content:// Uri,这样微信、备忘录才能正常接收:
Uri uri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", tempFile); Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, "分享日记"));FileProvider的配置路径在res/xml/file_paths.xml里,这一步很容易漏掉。
6. 项目演示与包装建议
6.1 演示数据准备
演示时屏幕上如果是一片空白,无论功能做得多完整,说服力都大打折扣。我写了一个测试数据填充工具,开发阶段点一下按钮自动生成50篇不同日期、不同心情、带标签、带图片的模拟日记。
模拟日记的内容要尽量真实,比如“今天天气很好,和朋友去了郊外的公园”,“工作项目进入关键阶段,加班到晚上十点”之类。这样演示时评委滑动列表,看到的是有生活气息的记录,而不是一行行“日记内容1”、“日记内容2”这样的占位文本。
这个工具放在设置页的一个隐藏入口,点击5次版本号调出开发者面板。正式答辩演示前,先用这个工具生成一批数据,列表页面、日历红点、搜索记录都会有很好的展示效果。
6.2 答辩展示的逻辑主线
准备答辩的时候,把重点放在三块:一是数据库设计层面的表结构规划和迁移策略,二是图片处理和内存优化(这是Android平台的经典技术难点),三是隐私保护方案。这三个点讲清楚,项目深度就有了。
演示操作路径建议:
- 打开App,输入锁屏PIN码或指纹解锁
- 主列表页展示日记卡片流,滑动翻页
- 点进一篇日记,展示标题、正文、图片、标签完整信息
- 编辑新日记,输入几行字,插入一张图片,保存
- 回到列表,看到新日记出现在顶部
- 用搜索框搜一个关键字,展示搜索结果
- 打开日历页,点击有红点的日期,跳转到当天日记
- 删除一篇日记,进入回收站,恢复数据
- 设置页导出HTML备份文件
- 分享单篇日记到其他App
这套流程走完,整个项目的完整功能链就全套展示过了,比讲PPT干说有力得多。
6.3 后续扩展方向
答辩中如果评委问“这个项目还有什么可以改进的地方”,可以提三个方向:
- 接入云同步,用网络数据库做多设备日记同步
- 引入手势密码或应用内独立PIN码,不依赖系统锁屏
- 增加Markdown编辑器支持,提升排版能力
- 加入时间轴或地理围栏,自动记录位置
我个人在实际操作中的体会是,电子日记本这个选题好就好在它麻雀虽小五脏俱全,覆盖了Android开发中绝大多数高频技术点:列表、编辑器、数据库、权限、文件存储、图片处理、消息通知。你做透这一个小项目,Android开发的绝大部分基础技能点就都过了一遍。
最后再分享一个小技巧:写编辑器的撤销/重做功能时,不要自己去维护一堆栈,直接使用EditText自带的能力,在每次setText之前保存一次游标位置和文本内容就可以实现。这个方法省代码,且对多数基础场景足够稳定。
项目做到这一步,已经具备了完整产品的基本形态。剩下的就是多拿几部不同品牌的机器跑一跑,把那些隐藏的适配问题揪出来,然后你就可以放心地在答辩台上展示了。