ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android电子日记本开发实战:数据库设计、图片处理与隐私保护

Android电子日记本开发实战:数据库设计、图片处理与隐私保护

最近帮一些准备毕业设计的朋友看选题,发现“基于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小型工具数据
RoomSQLite官方封装,结构清晰有一定的学习成本中大型结构化数据,首选
云端数据库多端同步依赖网络,域名部署复杂本地上传型需求,本项目不采用

实测下来,日记列表、标签、图片路径、用户配置这几类数据,用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 数据表设计与字段规划

日记本的核心表就是日记表。我设计的表结构如下:

字段名类型说明
idLONG主键,自增
titleTEXT标题,可空
contentTEXT正文内容
weatherTEXT天气,可空
moodTEXT心情,可空
tagsTEXT标签,JSON数组字符串
image_pathsTEXT图片路径,JSON数组
create_timeINTEGER创建时间(毫秒时间戳)
update_timeINTEGER修改时间(毫秒时间戳)
is_deleteINTEGER软删除标记,0正常,1回收站
is_draftINTEGER草稿标记

一个比较重要的经验:时间字段用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平台的经典技术难点),三是隐私保护方案。这三个点讲清楚,项目深度就有了。

演示操作路径建议:

  1. 打开App,输入锁屏PIN码或指纹解锁
  2. 主列表页展示日记卡片流,滑动翻页
  3. 点进一篇日记,展示标题、正文、图片、标签完整信息
  4. 编辑新日记,输入几行字,插入一张图片,保存
  5. 回到列表,看到新日记出现在顶部
  6. 用搜索框搜一个关键字,展示搜索结果
  7. 打开日历页,点击有红点的日期,跳转到当天日记
  8. 删除一篇日记,进入回收站,恢复数据
  9. 设置页导出HTML备份文件
  10. 分享单篇日记到其他App

这套流程走完,整个项目的完整功能链就全套展示过了,比讲PPT干说有力得多。

6.3 后续扩展方向

答辩中如果评委问“这个项目还有什么可以改进的地方”,可以提三个方向:

  • 接入云同步,用网络数据库做多设备日记同步
  • 引入手势密码或应用内独立PIN码,不依赖系统锁屏
  • 增加Markdown编辑器支持,提升排版能力
  • 加入时间轴或地理围栏,自动记录位置

我个人在实际操作中的体会是,电子日记本这个选题好就好在它麻雀虽小五脏俱全,覆盖了Android开发中绝大多数高频技术点:列表、编辑器、数据库、权限、文件存储、图片处理、消息通知。你做透这一个小项目,Android开发的绝大部分基础技能点就都过了一遍。

最后再分享一个小技巧:写编辑器的撤销/重做功能时,不要自己去维护一堆栈,直接使用EditText自带的能力,在每次setText之前保存一次游标位置和文本内容就可以实现。这个方法省代码,且对多数基础场景足够稳定。

项目做到这一步,已经具备了完整产品的基本形态。剩下的就是多拿几部不同品牌的机器跑一跑,把那些隐藏的适配问题揪出来,然后你就可以放心地在答辩台上展示了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进