简介:这份Android备忘录源码压缩包面向具备一定Android基础的开发者与移动应用学习者,提供一套可直接研读的完整项目实例,用于理解备忘录类应用的工程组织与核心功能实现。包内共190个文件,以74个png界面切图、61个class编译产物、22个xml布局与配置、20个java源码为主,另含prefs偏好设置、ogg提示音、apk与dex等运行文件,整体约1.29MB,体量轻便,便于快速导入Android Studio对照分析。源码覆盖Activity入口、SQLite数据库持久化、Adapter列表适配、ViewModel数据保持、Notification提醒以及AndroidManifest权限声明等典型模块,并附带drawable、values、styles等UI资源,可帮助读者梳理从界面布局到数据存储的完整链路。目前已有1802人学习下载,适合作为课程设计、练手项目或架构入门的参考素材,通过拆解目录结构与关键类,逐步掌握Android应用的构建思路与调试方法。
1. 从一份备忘录源码说起:Android 本地存储到底该怎么写
很多人第一次拿到「Android代码-备忘录源码.zip」这种资源,第一反应是解压、导入、跑起来,然后盯着界面看两眼就放下了。但真正值得看的不是界面,而是它怎么把一条条笔记落到本地、怎么在 Activity 重建时不丢数据、怎么在列表刷新时不出错。备忘录这个场景看着简单,实际上把 Android 四大组件、SQLite、RecyclerView、权限、生命周期全串了一遍,是练手本地存储最合适的载体。我见过不少同学能背出 SQLiteOpenHelper 的四个回调,却写不出一个能稳定增删改查的备忘录,问题就出在没把「数据流」和「生命周期」对齐。这篇笔记就按一份典型备忘录源码的结构,把能复现的路径、参数和踩坑点讲清楚,适合刚学完基础组件想找个完整小项目练手的开发者,也适合想回头补本地存储基本功的熟手。
2. 备忘录源码的骨架:先看清数据从哪来到哪去
一份能跑的备忘录源码,通常不会只有一个 Activity。它至少包含三个层次:界面层负责展示和接收输入,数据层负责持久化,模型层负责在两者之间传递。很多人导入源码后直接改布局,改完发现数据对不上,就是因为没先理清这三层怎么连。
2.1 三个核心类:Activity、Adapter、SQLiteOpenHelper
界面层一般是一个列表 Activity 加一个编辑 Activity。列表 Activity 持有 RecyclerView 和 Adapter,编辑 Activity 负责接收标题和内容,点保存后写库并通知列表刷新。数据层是一个继承 SQLiteOpenHelper 的类,里面定义建表语句和增删改查方法。模型层就是一个 Note 实体类,字段通常有 id、title、content、createTime、updateTime。
先看模型类,它决定了后面所有方法的参数形态:
public class Note { private long id; private String title; private String content; private long createTime; private long updateTime; // 无参构造给查询结果用,带参构造给新建用 public Note() {} public Note(String title, String content) { this.title = title; this.content = content; this.createTime = System.currentTimeMillis(); this.updateTime = this.createTime; } // getter/setter 省略 }这里有个容易忽略的点:createTime 和 updateTime 分开存。很多新手只存一个时间,结果编辑后排序就乱了。分开存之后,列表默认按 updateTime 倒序,最近改过的排最前,符合备忘录的使用直觉。
再看 SQLiteOpenHelper,建表语句和版本号是重点:
public class NoteDbHelper extends SQLiteOpenHelper { private static final String DB_NAME = "note.db"; private static final int DB_VERSION = 1; public NoteDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE note (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "title TEXT NOT NULL, " + "content TEXT, " + "create_time INTEGER, " + "update_time INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级时先删表再重建,仅适合开发阶段 db.execSQL("DROP TABLE IF EXISTS note"); onCreate(db); } }参数说明:DB_VERSION 从 1 开始,每次改表结构就加 1,否则 onUpgrade 不会触发。onUpgrade 里直接删表是开发期做法,上线后必须改成 ALTER TABLE 迁移,不然用户数据全丢。id 用 AUTOINCREMENT 保证删除后新增不会复用旧 id,避免列表错位。
2.2 增删改查四个方法的参数怎么定
数据层的方法签名直接决定调用方怎么写。常见做法是每个方法接收 Note 对象或 id,返回 long 或 List。插入返回新行 id,更新和删除返回受影响行数,查询返回列表。
public long insert(Note note) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put("title", note.getTitle()); values.put("content", note.getContent()); values.put("create_time", note.getCreateTime()); values.put("update_time", note.getUpdateTime()); return db.insert("note", null, values); } public List<Note> queryAll() { List<Note> list = new ArrayList<>(); SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query("note", null, null, null, null, null, "update_time DESC"); while (cursor.moveToNext()) { Note note = new Note(); note.setId(cursor.getLong(cursor.getColumnIndexOrThrow("id"))); note.setTitle(cursor.getString(cursor.getColumnIndexOrThrow("title"))); note.setContent(cursor.getString(cursor.getColumnIndexOrThrow("content"))); note.setUpdateTime(cursor.getLong(cursor.getColumnIndexOrThrow("update_time"))); list.add(note); } cursor.close(); return list; }逻辑说明:insert 用 ContentValues 而不是拼 SQL 字符串,避免注入和转义问题。queryAll 的排序参数写 "update_time DESC",让最近修改的排前面。cursor 用完必须 close,否则反复查询会泄漏。getColumnIndexOrThrow 比 getColumnIndex 好,列名写错时直接抛异常,不会静默返回 -1。
2.3 列表刷新:为什么改完数据界面没变
Adapter 的数据源是一个 List,查询返回新 List 后,如果直接赋值给成员变量而不通知 Adapter,界面不会更新。正确做法是清空再加、然后 notifyDataSetChanged,或者用 DiffUtil 做增量更新。
private void reloadNotes() { noteList.clear(); noteList.addAll(dbHelper.queryAll()); adapter.notifyDataSetChanged(); }注意:notifyDataSetChanged 会重绘整个列表,数据量大时卡顿。备忘录条目一般不多,够用。如果条目上千,换成 DiffUtil 或 ListAdapter。另外,编辑 Activity 保存后要通知列表 Activity 刷新,常见做法是用 startActivityForResult 或 ActivityResultLauncher,在回调里调 reloadNotes。
3. 把源码跑起来:从导入到第一条笔记落库
理清骨架后,动手跑一遍才能发现细节问题。这一章按导入、建库、写第一条、验证四步走,每步都给可抄的命令或代码。
3.1 导入前的环境检查与 Gradle 配置
导入前先确认本地 Android Studio 版本和 Gradle 插件版本匹配。源码里的 build.gradle 如果写的是老版本插件,直接同步会报错。常见做法是打开项目级 build.gradle,把插件版本改成和本地 Studio 匹配的版本,再同步。
# 查看本地已安装的 Gradle 版本 gradle -v # 如果项目用的是 wrapper,直接跑 wrapper 命令 ./gradlew tasks参数说明:gradlew 是项目自带的 wrapper,版本由 gradle-wrapper.properties 里的 distributionUrl 决定。如果下载慢,可以换成国内镜像地址,但要注意镜像的 Gradle 版本要和插件兼容。同步失败时先看 Build 窗口的报错行,多数是 compileSdk 或 targetSdk 写得太新或太旧。
3.2 建库与建表:onCreate 只跑一次这件事
SQLiteOpenHelper 的 onCreate 只在数据库文件不存在时调用一次。如果第一次跑的时候建表语句写错,后面改对了也不会重新执行,因为文件已经存在。血泪经验:调试阶段改表结构,要么改 DB_VERSION 触发 onUpgrade,要么在设备上卸载应用重装。
// 调试期可以在 Application 或主 Activity 里临时删库 context.deleteDatabase("note.db");注意:这行只能放调试代码里,上线前删掉。删库后下次 getWritableDatabase 会重新走 onCreate。
3.3 写第一条笔记:插入与查询的完整链路
在编辑 Activity 里点保存,走的是「取输入 → 构造 Note → 调 insert → 关页面 → 通知刷新」这条链路。
saveBtn.setOnClickListener(v -> { String title = titleEt.getText().toString().trim(); String content = contentEt.getText().toString().trim(); if (title.isEmpty()) { Toast.makeText(this, "标题不能为空", Toast.LENGTH_SHORT).show(); return; } Note note = new Note(title, content); long id = dbHelper.insert(note); if (id > 0) { setResult(RESULT_OK); finish(); } else { Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show(); } });逻辑说明:title 做非空校验,content 允许为空。insert 返回 -1 表示失败,通常是约束冲突或磁盘问题。setResult 配合列表 Activity 的 launcher 回调触发刷新。这里没在主线程做耗时查询,insert 本身很快,但如果有大量数据或复杂事务,要放到子线程。
3.4 用 App Inspection 验证数据真的落库了
Android Studio 自带 Database Inspector,可以在应用运行时直接看 note.db 里的数据。打开方式:View → Tool Windows → App Inspection → Database Inspector,选中进程后展开 databases。
| 检查项 | 正常表现 | 异常表现与排查 |
|---|---|---|
| 数据库文件 | 出现 note.db | 没有文件说明 getWritableDatabase 没调用 |
| note 表 | 表结构含 5 个字段 | 字段缺失说明 onCreate 没执行或版本没升 |
| 数据行 | 新增后多一行 | 行数不变说明 insert 返回 -1 |
| update_time | 每次编辑后变大 | 不变说明更新方法没写 update_time |
这个表建议对照着看,比盲猜快得多。如果 Database Inspector 连不上,先确认设备是 API 26 以上,低版本不支持。
4. 避坑与排查:备忘录源码里最容易翻车的五处
跑通之后,真正花时间的是那些「看着对但就是不对」的问题。下面五条是我在类似项目里反复遇到的,按现象、原因、解决写清楚。
4.1 旋转屏幕后输入框内容消失
现象:在编辑页输入一半,旋转屏幕,标题和内容全没了。原因:Activity 重建,成员变量被重置,而 EditText 的内容如果没有保存状态就会丢。解决:给 EditText 加 android:id,系统会自动保存;或者在 onSaveInstanceState 里手动存,onCreate 里恢复。
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("title", titleEt.getText().toString()); outState.putString("content", contentEt.getText().toString()); }注意:手动保存要在 setContentView 之后、恢复逻辑之前理清顺序,否则取到空控件。
4.2 列表点击编辑后数据错位
现象:点第 3 条编辑,改完保存,结果第 1 条变了。原因:Adapter 里用 position 当 id 传给编辑页,但列表排序变了或数据刷新后 position 和 id 不再对应。解决:传 note.getId(),编辑页按 id 查询和更新,不要用 position。
// 错误做法 intent.putExtra("position", position); // 正确做法 intent.putExtra("note_id", noteList.get(position).getId());4.3 数据库版本升级后应用崩溃
现象:改了表结构,DB_VERSION 加 1,运行后查询报 no such column。原因:onUpgrade 里只删了表没重建,或者重建语句和查询字段不一致。解决:onUpgrade 里先 DROP 再 onCreate,并确保查询用的列名和新表一致。上线版本要用 ALTER TABLE 逐版本迁移,不能删表。
4.4 主线程查询导致列表卡顿
现象:笔记多了以后,打开列表页要等一两秒才显示。原因:queryAll 在主线程执行,数据量大时阻塞 UI。解决:把查询放到子线程,用 ExecutorService 或 Kotlin 协程,查完再切主线程更新 Adapter。
ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { List<Note> notes = dbHelper.queryAll(); runOnUiThread(() -> { noteList.clear(); noteList.addAll(notes); adapter.notifyDataSetChanged(); }); });参数说明:newSingleThreadExecutor 保证查询串行,避免并发写库冲突。runOnUiThread 把结果切回主线程更新 UI。如果项目用 Kotlin,直接 viewModelScope + Dispatchers.IO 更简洁。
4.5 忘记关闭 Cursor 导致内存泄漏
现象:反复进出列表页,内存占用持续上涨。原因:Cursor 没 close,底层游标资源没释放。解决:用 try-finally 包住,或者用 try-with-resources(API 16 以上 Cursor 实现了 Closeable)。
try (Cursor cursor = db.query("note", null, null, null, null, null, "update_time DESC")) { while (cursor.moveToNext()) { // 读取数据 } }注意:try-with-resources 会自动 close,比手动写 finally 更稳。如果查询方法返回 Cursor 给调用方,关闭责任要明确,别两边都不关。
5. 从能跑到好用:三个让备忘录更稳的技巧
跑通和排查之后,如果还想把这个方向做扎实,有三个技巧值得花时间。它们不改变功能,但决定这个备忘录能不能长期用。
第一个是给数据库加索引。备忘录列表按 update_time 排序,数据量上千后排序会变慢。在 onCreate 里加一句 CREATE INDEX idx_update_time ON note(update_time),查询排序就走索引。索引不是越多越好,写操作会变慢,备忘录这种读多写少的场景加一个排序索引刚好。
db.execSQL("CREATE INDEX idx_update_time ON note(update_time)");第二个是用事务包住批量操作。如果以后加导入功能,一次插几百条,逐条 insert 会频繁开事务,慢且容易中途失败。用 beginTransaction 和 setTransactionSuccessful 包起来,要么全成功要么全回滚。
db.beginTransaction(); try { for (Note note : notes) { insert(note); } db.setTransactionSuccessful(); } finally { db.endTransaction(); }第三个是给查询加限制。列表页一次只显示 50 条,滑到底再加载更多。query 方法的 limit 参数写 "50",配合 offset 做分页。这样即使有几千条笔记,打开列表也是秒开。分页的 offset 用 (page - 1) * pageSize 算,page 从 1 开始。
| 技巧 | 适用场景 | 关键参数 | 效果 |
|---|---|---|---|
| 加索引 | 列表排序变慢 | update_time 列 | 排序查询走索引 |
| 用事务 | 批量插入/更新 | beginTransaction | 减少 IO,保证原子性 |
| 分页查询 | 数据量上千 | limit + offset | 首屏加载快 |
最后说个我自己的习惯:每次改完数据层方法,先在 Database Inspector 里手动跑一遍增删改查,确认数据对得上,再去改界面。界面问题十有八九是数据层没写对,先看数据能省很多来回。备忘录源码的价值不在界面多好看,而在它把本地存储这条链路完整暴露出来,照着改一遍、踩一遍坑,比看十篇原理文章都管用。希望帮到你。
本文还有配套的精品资源,点击获取