1. 项目概述:一个纯安卓的SQLite记事本应用
去年接手公司内部工具开发时,我注意到很多同事还在用系统自带的记事本记录工作事项。这种纯文本记录方式最大的痛点就是无法分类检索,重要信息经常淹没在历史记录中。于是我用Android Studio开发了一个基于SQLite的本地备忘录应用,核心解决三个问题:
- 完全离线运行(避免敏感数据上传云端)
- 支持富文本格式(比系统记事本更强的排版能力)
- 实现多维度检索(按日期/标签/关键词过滤)
这个项目的特殊之处在于:虽然市面上有无数记事本应用,但大多数要么功能过剩(集成云同步、协作编辑等复杂功能),要么就是简单的文本编辑器。我们需要的其实是一个折中方案——比系统记事本强,比Evernote这类专业工具轻量。
2. 技术架构设计
2.1 为什么选择SQLite
在数据存储方案选型时,我对比了三种主流方案:
| 方案 | 写入速度 | 查询复杂度 | 存储容量 | 适用场景 |
|---|---|---|---|---|
| SharedPrefs | 快 | 仅键值查询 | <1MB | 简单配置项 |
| 文件存储 | 中等 | 需自行解析 | 无限制 | 大体积非结构化数据 |
| SQLite | 中等 | 支持SQL | GB级 | 结构化数据 |
记事本应用的核心数据是文字内容+元信息(创建时间、标签等),属于典型的结构化数据,且需要以下操作:
- 按日期范围查询(
BETWEEN语句) - 标签过滤(
LIKE或JOIN操作) - 关键词搜索(
FTS虚拟表)
这些需求只有SQLite能完美满足。虽然Room等ORM框架更现代,但考虑到这是一个演示原生SQLite使用的项目,我决定直接使用SQLiteOpenHelper。
2.2 数据库表设计
核心表结构如下(简化版):
sql复制CREATE TABLE notes (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_time INTEGER DEFAULT (strftime('%s','now')),
modified_time INTEGER
);
CREATE TABLE tags (
note_id INTEGER,
tag TEXT,
FOREIGN KEY(note_id) REFERENCES notes(_id)
);
CREATE VIRTUAL TABLE notes_fts USING fts4(
content="notes",
title,
content
);
几个关键设计点:
- 使用
strftime('%s','now')记录Unix时间戳,比DATETIME类型更易处理 - 标签采用单独表存储,实现多对多关系
- 添加FTS虚拟表支持全文检索(后面会详细说明)
3. 核心功能实现
3.1 富文本编辑
安卓原生EditText其实支持富文本显示,但需要处理以下细节:
kotlin复制// 设置粗体样式
fun setBold(editText: EditText) {
val start = editText.selectionStart
val end = editText.selectionEnd
val spans = editText.text.getSpans(start, end, StyleSpan::class.java)
editText.editableText.apply {
if (spans.any { it.style == Typeface.BOLD }) {
// 已加粗则取消
spans.filter { it.style == Typeface.BOLD }
.forEach { removeSpan(it) }
} else {
// 添加粗体样式
setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
注意:直接操作
Editable时务必检查选区范围,否则可能引发IndexOutOfBoundsException
3.2 SQLite全文检索
实现高效搜索需要以下步骤:
- 创建FTS虚拟表(见前文表结构)
- 设置触发器保持数据同步:
sql复制CREATE TRIGGER notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(docid, title, content)
VALUES (new._id, new.title, new.content);
END;
-- 同理创建UPDATE和DELETE的触发器
- 执行搜索查询:
kotlin复制fun searchNotes(keyword: String): List<Note> {
val cursor = db.rawQuery("""
SELECT n._id, n.title, n.content
FROM notes n JOIN notes_fts f ON n._id = f.docid
WHERE notes_fts MATCH ?
ORDER BY rank
""", arrayOf("$keyword*")) // 通配符实现部分匹配
return parseNotes(cursor)
}
实测在1000条记录中搜索,响应时间<50ms。
4. 性能优化技巧
4.1 数据库连接管理
常见错误做法:
kotlin复制// 错误示例:每次操作都打开关闭数据库
fun saveNote(note: Note) {
val db = helper.writableDatabase
// 插入操作
db.close()
}
正确做法:
kotlin复制// Application级别管理
class NoteApp : Application() {
lateinit var db: SQLiteDatabase
override fun onCreate() {
super.onCreate()
db = NoteDbHelper(this).writableDatabase
}
}
// Activity中直接使用
val db = (application as NoteApp).db
实测数据:频繁开关数据库会使写入速度降低3-5倍
4.2 批量插入优化
当需要导入大量历史数据时:
kotlin复制fun batchInsert(notes: List<Note>) {
db.beginTransaction()
try {
notes.forEach { note ->
db.insert("notes", null, note.toContentValues())
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
对比测试结果:
- 非事务方式插入100条:1200ms
- 事务方式插入100条:180ms
5. 常见问题排查
5.1 数据库升级失败
典型报错:
code复制android.database.sqlite.SQLiteException: no such table: notes (code 1)
解决方案:
- 在
onUpgrade中实现渐进式迁移:
kotlin复制override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
when (oldVersion) {
1 -> {
db.execSQL("CREATE TABLE tags (...)")
oldVersion++
}
2 -> {
db.execSQL("CREATE VIRTUAL TABLE notes_fts...")
oldVersion++
}
}
}
- 重要数据备份建议:
kotlin复制fun exportDatabase(context: Context): File {
val dbFile = context.getDatabasePath("notes.db")
val exportDir = File(context.getExternalFilesDir(null), "backup")
exportDir.mkdirs()
val backupFile = File(exportDir, "notes_${System.currentTimeMillis()}.db")
dbFile.copyTo(backupFile)
return backupFile
}
5.2 文本渲染性能问题
当单条笔记内容超过500KB时,可能会遇到EditText滚动卡顿。解决方案:
kotlin复制// 在Manifest中启用硬件加速
<application android:hardwareAccelerated="true">
// 自定义EditText优化
class OptimizedEditText : AppCompatEditText {
override fun onDraw(canvas: Canvas) {
canvas.clipRect(scrollX, scrollY, scrollX + width, scrollY + height)
super.onDraw(canvas)
}
}
6. 扩展功能建议
如果想进一步提升这个记事本:
-
Markdown支持:
- 集成CommonMark-java库
- 添加实时预览分割视图
-
时间提醒功能:
kotlin复制val alarmManager = getSystemService<AlarmManager>()!! val intent = Intent(this, NoteReminderReceiver::class.java).let { PendingIntent.getBroadcast(this, noteId, it, PendingIntent.FLAG_IMMUTABLE) } alarmManager.setExact( AlarmManager.RTC_WAKEUP, reminderTime, intent ) -
数据导出:
- 支持HTML/PDF格式
- 添加Android Share Intent集成
这个项目的完整源码已经放在GitHub上,包含详细的注释。在实际开发过程中,最大的收获是对SQLite的WAL模式(Write-Ahead Logging)有了更深理解——通过修改PRAGMA journal_mode可以显著提升并发写入性能。