资讯动态

Android记事本APP开发实战:SQLite增删改查与源码包设计

发布时间:2026/10/14 17:28:43 来源:尧图企业网站定制
做记事本类Android项目我前后折腾过不下三版。老实说功能看起来就是“增删改查 SQLite数据库”连需求文档都能写满一张纸但真把一个可交付的备忘录APP源码包完整跑起来里面藏的坑比想象中多得多表结构怎么设计才方便后期限扩充、数据库Helper该不该单例、游标什么时候关闭、列表刷新时机怎么卡、日期字段用什么格式存、整个源码包目录怎么组织才能让别人打开Android Studio就能跑。这篇就把我手里这套“基于Android Studio的记事本备忘录APP开发全套源码包”从设计到落地的完整过程拆开讲SQLite数据库层的增删改查实现、界面层和数据库怎么联动、源码包结构怎么复用、以及我实测踩过的几个典型问题都写清楚。适合刚学Android需要练手项目的新人也想给准备做毕业设计、课设或者给GitHub开源仓库做源码整理的开发者做个参考。1. 项目思路与整体设计1.1 为什么记事本是最合适的SQLite练手载体很多人在学Android数据库时会直接堆一个复杂的业务系统比如订单管理、图书管理结果数据库表设计还没理清楚界面先把人折腾废了。记事本备忘录是一个恰到好处的载体单表单对象、字段少标题、内容、创建时间、更新时间、操作逻辑清晰增删改查但它又能把Android开发里的关键环节全部串起来——Activity生命周期对数据刷新影响、RecyclerView适配、空数据页状态、对话框交互、数据库升级、资源文件管理全都在一个不大不小的项目里覆盖到了。我选SQLite而不是Room或者GreenDAO的理由也很直接SQLite是Android原生内置的数据库不需要额外引入依赖库SQL语句自己写一遍数据库底层工作原理就清楚了。Room封装的太好新手往往只知道注解注解注解数据库文件长什么样都不知道。做源码包的话用裸SQLite也最容易让接手的人看懂改懂。这套源码包在设计上遵循了一个很朴素的原则能用原生组件不引框架能画清楚就不过度设计。主界面就是一个RecyclerView长按弹菜单点击进编辑页新建按钮跳新的Activity保存后刷新列表。不搞Fragment嵌套不搞ViewModel和数据绑定的全家桶每个类都有单一职责初学者拿过来能顺着代码一步步读懂有经验的朋友也能快速改造。1.2 数据模型与表结构设计备忘录的核心数据模型很简单就是一个Note对象我给它设计了五个字段分别是主键id、标题、内容、创建时间createTime、更新时间updateTime。id用的是Long型自增主键标题和内容用String时间用Long型的毫秒值保存。public class Note { private Long id; private String title; private String content; private Long createTime; private Long updateTime; public Note() { } public Note(Long id, String title, String content, Long createTime, Long updateTime) { this.id id; this.title title; this.content content; this.createTime createTime; this.updateTime updateTime; } // 省略 getter/setter }有人问为什么时间不用String类型直接存“2025-03-14 21:33:12”我早期版本确实这样干过后来发现两个痛点第一SQLite里字符串比较大小是按字典序走的如果在查询时用WHERE update_time 某个字符串做时间过滤格式稍微不统一就会出问题第二UI展示需要的时间格式跟数据库存储格式往往不一致用Long毫秒值存展示层想格式化成什么样子随心所欲计算时间差也方便。建表语句我放在了数据库Helper里命名为noteSQL如下CREATE TABLE note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL );这里有两个细节值得注意。title我加了NOT NULL约束这样即使用户不填标题保存时也能在逻辑层做判断提示而不是等到数据库报错。content允许为空因为有些用户就喜欢记一个空内容的备忘只留一个标题也说得过去。update_time单独设置字段用于排序和后续可能做的“最近编辑”展示createTime一旦创建就不允许再改。1.3 界面交互方案选型交互方案我对比过三种第一种是主界面直接显示List旁边放悬浮新增按钮第二种是底部导航栏分“全部笔记”和“收藏”两个页签第三种是带搜索框的复杂方案。这套源码包选了第一种原因很务实增删改查的演示重点是数据库操作界面交互保持简洁不把学习成本浪费在非核心模块上。主界面布局由三部分组成顶部的标题栏、中间的RecyclerView区域、右下角的FloatingActionButton。列表项note_item.xml只有两个控件一个显示标题的TextView一个显示更新时间的TextView点击后跳编辑页。长按弹出AlertDialog提供“编辑”和“删除”两个选项这样把入口统一收拢用户不需要记住列表项上哪个图标是删除、哪个是编辑。新增笔记和编辑笔记共用一个Activity只在启动时通过Intent传参数区分。新建时传-1编辑时传笔记idActivity读取id后判断是走“插入”还是“更新”流程。这个设计能省掉一半界面代码对新手也友好一个Activity写好了两条路径都通了。2. 工程搭建与环境准备2.1 Android Studio版本与SDK配置建议整套源码包我是在Android Studio较新的版本下开发的但为了保证任何人打开都能直接跑build配置我往下兼容了不少。建议的编译SDK版本是33或34minSdk设置成21也就是Android 5.0起步。覆盖了当前主流设备的系统版本同时minSdk设得低一些代码里就需要避开太新的API好在这个项目基本用不到什么特殊接口普通写法就够了。经验minSdk别一上来就设28甚至30。很多新手机没问题但如果你要跑老机型的模拟器或者别人的手机系统版本不高minSdk太高就直接装不上。设到21兼容和现代感都有。依赖方面保持极简实际上核心功能全部用Android原生SDK就够了我只加了一个AndroidX的appcompat和material库。RecyclerView依赖的是androidx.recyclerview:recyclerviewMaterial库提供FloatingActionButton和AlertDialog。不加其他第三方库这种源码包的好处是依赖越少出诡异编译问题的概率越低别人下载下来不需要等Gradle长时间拉一堆库。工程结构按功能分包顶层目录如下app/src/main/java/com/example/notepad/ ├── activity/ │ ├── MainActivity.java │ └── EditNoteActivity.java ├── adapter/ │ └── NoteAdapter.java ├── database/ │ ├── NoteDbHelper.java │ └── NoteDao.java ├── model/ │ └── Note.java └── utils/ └── TimeUtils.java这个分包方式不是标准答案但对我来说清晰了五年。activity放界面层adapter放列表适配database放数据库相关的Helper和DAOmodel放实体utils放通用工具。性能更好的分层架构方案很多但面对一个练手或者交作业级别的项目分层太多反而是负担。2.2 工程迁移与导入的几个前提检查源码包发出去以后最常收到的反馈是“我怎么打不开”“一堆红叉”。排查下来一半以上是环境问题。拿到这套源码包后建议你先检查三样东西Gradle JDK版本。新版Android Studio默认自带JDK17但老项目用的Gradle版本如果太旧可能要求JDK8或11。如果报Unsupported Java的错优先调Android Studio里的Gradle JDK设置而不是换IDE。Gradle版本与Android Gradle Plugin版本匹配。我用的AGP版本在gradle-wrapper.properties里能看新人最常犯的错是直接把Android Studio自动生成的新项目Gradle文件覆盖过来导致依赖版本不匹配。SDK Location。Android Studio打开后如果提示SDK找不到检查local.properties文件里的sdk.dir路径是否正确指向本机SDK目录。这三步确认完工程基本就能丝滑导入。遇到过很多朋友卡在环境上其实项目代码本身没毛病。2.3 权限声明与AndroidManifest配置备忘录APP不需要联网权限不需要读取存储卡权限在Android 6.0及以上的动态权限申请环节也能省掉。Manifest里面的配置非常干净application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:roundIconmipmap/ic_launcher_round android:supportsRtltrue android:themestyle/Theme.Notepad activity android:name.activity.EditNoteActivity android:windowSoftInputModeadjustResize / activity android:name.activity.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /applicationwindowSoftInputModeadjustResize这个属性容易被忽略但实际体验影响巨大。没有它EditText唤起软键盘时会把整个布局顶上去保存按钮就被挤到键盘后面。设置为adjustResize后系统会在键盘弹出时调整窗口大小布局能自适应用户才能一边打字一边看到保存按钮。3. 数据库层的完整实现3.1 SQLiteOpenHelper子类设计数据库访问层我写成两个类按职责拆开NoteDbHelper负责数据库文件的创建、升级和连接管理NoteDao负责与笔记表相关的增删改查SQL操作。这样做是让数据库连接的管理和业务操作互不干扰测试和维护都好下手。NoteDbHelper的完整实现如下public class NoteDbHelper extends SQLiteOpenHelper { private static final String DB_NAME notepad.db; private static final int DB_VERSION 1; private static volatile NoteDbHelper instance; private NoteDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } public static NoteDbHelper getInstance(Context context) { if (instance null) { synchronized (NoteDbHelper.class) { if (instance null) { instance new NoteDbHelper(context.getApplicationContext()); } } } return instance; } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, create_time INTEGER NOT NULL, update_time INTEGER NOT NULL)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { // 此处做示例升级逻辑比如新增 category 字段 db.execSQL(ALTER TABLE note ADD COLUMN category TEXT DEFAULT default); } } }关于单例的双重检查锁多说一句。SQLiteOpenHelper的构造函数本身不昂贵真正耗时的是第一次getWritableDatabase时创建数据库文件。但如果不加单例在MainActivity和EditNoteActivity之间频繁切换时每次都new一个Helper实例会导致多个数据库连接同时存在某些场景下容易出现SQLiteDatabase object was closed的诡异报错。单例模式结合getApplicationContext还能避免Activity造成的内存泄漏属于写好一次、省心一整年的操作。3.2 增删改查SQL语句的封装细节NoteDao相当于数据访问对象所有跟note表打交道的SQL操作都收敛在这里。我实际封装了五个方法insertNote、updateNote、deleteNote、queryAllNotes、queryNoteById。其中增删改都是返回布尔值查询返回List。public class NoteDao { private NoteDbHelper helper; public NoteDao() { helper NoteDbHelper.getInstance(MyApplication.getContext()); } public boolean insertNote(Note note) { SQLiteDatabase db helper.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()); long rowId db.insert(note, null, values); return rowId ! -1; } public boolean updateNote(Note note) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(title, note.getTitle()); values.put(content, note.getContent()); values.put(update_time, note.getUpdateTime()); int rows db.update(note, values, id ?, new String[]{String.valueOf(note.getId())}); return rows 0; } public boolean deleteNote(Long id) { SQLiteDatabase db helper.getWritableDatabase(); int rows db.delete(note, id ?, new String[]{String.valueOf(id)}); return rows 0; } public ListNote queryAllNotes() { ListNote noteList new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query(note, null, null, null, null, null, update_time DESC); if (cursor ! null) { 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.setCreateTime(cursor.getLong(cursor.getColumnIndexOrThrow(create_time))); note.setUpdateTime(cursor.getLong(cursor.getColumnIndexOrThrow(update_time))); noteList.add(note); } cursor.close(); } return noteList; } public Note queryNoteById(Long id) { SQLiteDatabase db helper.getReadableDatabase(); Note note null; Cursor cursor db.query(note, null, id ?, new String[]{String.valueOf(id)}, null, null, null); if (cursor ! null cursor.moveToFirst()) { 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.setCreateTime(cursor.getLong(cursor.getColumnIndexOrThrow(create_time))); note.setUpdateTime(cursor.getLong(cursor.getColumnIndexOrThrow(update_time))); cursor.close(); } return note; } }我在queryAllNotes里用了update_time DESC作为排序规则。这是产品层面的取舍用户打开备忘录第一时间应该看到最近更新的笔记而不是创建最早的笔记。用updateTime排序能保证你改完一条旧笔记它立刻出现在列表最上方操作反馈非常直观。Cursor获取列索引用的是getColumnIndexOrThrow而不是getColumnIndex区别在于前者在列名不存在时会直接抛异常方便在开发期就发现SQL和实体字段不一致的问题而不是返回一个-1索引然后getString(-1)抛个让人摸不着头脑的崩溃。3.3 事务处理与批量删除的扩展点基础增删改查做完之后我特意在源码包里留了一个事务批处理的演示方法源码包的意义要大于能跑就行。虽然当前界面上没有批量删除入口但这个方法被注释掉了写在NoteDao里方便需要扩展的人参考。public boolean deleteNotesInTransaction(ListLong ids) { SQLiteDatabase db helper.getWritableDatabase(); boolean result false; db.beginTransaction(); try { for (Long id : ids) { db.delete(note, id ?, new String[]{String.valueOf(id)}); } db.setTransactionSuccessful(); result true; } catch (Exception e) { e.printStackTrace(); } finally { db.endTransaction(); } return result; }事务的好处不需要我多说多个删除操作要么全部成功要么全部失败。这里真正的教训是不要嵌套事务也不要忘记在finally里调用endTransaction。源码包附带的注释里我写过一句话——你永远不希望用户点了删除五条笔记结果删了前三条后手机没电数据库留在半路状态。实操心得SQLite的事务开销很低批量操作改成事务后耗时能下降一个量级。早期我循环一万条insert不加事务跑了五十多秒加上事务后同一台测试机不到三秒。这差距是肉眼可见的。4. 界面层与数据库的联动实现4.1 主界面列表适配与数据刷新MainActivity的onCreate里先初始化RecyclerView设置LinearLayoutManager再创建NoteAdapter最后调用loadNotesData()从数据库查询数据并填充列表。这套流程是Android列表界面的固定套路但有几个细节值得抠。private void loadNotesData() { noteList noteDao.queryAllNotes(); if (noteAdapter null) { noteAdapter new NoteAdapter(this, noteList); recyclerView.setAdapter(noteAdapter); } else { noteAdapter.setNoteList(noteList); noteAdapter.notifyDataSetChanged(); } handleEmptyView(); }这里我特意让加载数据的方法既承担首次初始化也承担刷新功能而不是刷新时重新new一个Adapter。如果每次刷新都重新new Adapter表面上看起来没毛病但列表滚动位置会回到顶部用户体验就是“我刚滑到列表中间编辑了一条笔记返回后列表跳到了第一条”非常割裂。用同一个Adapter实例加notifyDataSetChanged能把滚动位置保留在原地。Adapter这边我把点击事件跟长按事件都暴露出去通过接口回调到Activity处理不在Adapter内部弹窗。这样Adpater保持纯粹只负责把数据渲染到View上交互逻辑留在Activity里代码读起来顺。4.2 空数据状态的友好处理空数据这块很多新手会忽略觉得数据库没数据就不管了。但一个备忘录APP如果刚安装后打开是白屏用户第一反应是软件坏了。我在主界面的布局里放了一个空的TextView和一张简单图标平时GONE隐藏只有当noteList为空时visible显示出来。private void handleEmptyView() { if (noteList null || noteList.isEmpty()) { recyclerView.setVisibility(View.GONE); tvEmptyTip.setVisibility(View.VISIBLE); } else { recyclerView.setVisibility(View.VISIBLE); tvEmptyTip.setVisibility(View.GONE); } }这条逻辑成本极低但对整体印象的加成很明显。以前给一些项目做代码审查经常看到有人把空状态做成一张没有文字提示的空白图用户不知道是加载失败还是真的没数据。一个“还没有笔记点右下角新建一条吧”的文案加上轻量图标比任何复杂的引导页都实在。4.3 新增与编辑页面的数据回显EditNoteActivity是整个项目里交互细节最密集的地方。新建和编辑两种模式共用同一个布局EditText标题、多行EditText内容、保存按钮。区别只在onCreate里从Intent读取noteId如果noteId不等于-1说明是编辑模式需要先调queryNoteById把数据回显到输入框。long noteId getIntent().getLongExtra(note_id, -1); if (noteId ! -1) { Note note noteDao.queryNoteById(noteId); if (note ! null) { etTitle.setText(note.getTitle()); etContent.setText(note.getContent()); } }回显有个坑如果你在setText之后光标的默认位置是在文本开头。用户想改标题末尾的字点进去发现光标在前面每次都要手动点一下末尾这体验就不太好。解决办法是把选择位置设置到文本末尾etTitle.setSelection(etTitle.getText().length());保存按钮的逻辑同样区分模式。新建模式下new一个Note对象设置当前时间为createTime和updateTime调insertNote编辑模式下读取原Note对象保留createTime不变更新title、content和updateTime调updateNote。时间字段的区分处理是数据质量的关键——如果编辑时把createTime也改了后续如果做“按创建时间排序”或者“查看创建日期”数据就全乱了。4.4 长按菜单删除与二次确认删除在交互上必须做二次确认否则用户误触就丢数据。我在OnLongClickListener里弹了一个AlertDialog选项是编辑和删除。选择删除时再弹出一个Dialog提示“确定要删除这条笔记吗”确认才真正调deleteNote。Override public void onLongClick(Note note) { String[] items {编辑, 删除}; new AlertDialog.Builder(MainActivity.this) .setTitle(note.getTitle()) .setItems(items, (dialog, which) - { if (which 0) { Intent intent new Intent(MainActivity.this, EditNoteActivity.class); intent.putExtra(note_id, note.getId()); startActivity(intent); } else if (which 1) { confirmDelete(note); } }) .show(); } private void confirmDelete(Note note) { new AlertDialog.Builder(MainActivity.this) .setMessage(确定要删除这条笔记吗) .setPositiveButton(删除, (dialog, which) - { noteDao.deleteNote(note.getId()); loadNotesData(); }) .setNegativeButton(取消, null) .show(); }为什么不用直接调删除接口的Snackbar带撤销功能我承认Snackbar的Undo体验更现代但这套源码包的定位更偏基础和稳定AlertDialog是每个Android初学者都能秒懂的标准交互。你要扩展Undo代码里留了接口替换成本不高。4.5 生命周期对数据同步的影响MainActivity的onResume里调用loadNotesData()是我反复考量后的选择。一开始我把刷新放在EditNoteActivity的finish之后通过startActivityForResult返回结果再刷新代码看起来严谨但分支特别多按返回键退出时要不要刷新保存成功刷新没保存但改了内容呢换成onResume刷新是省心方案不管用户怎么退出编辑页MainActivity从后台回到前台、从编辑页返回、甚至从最近任务列表切回来都会执行刷新。数据永远跟数据库同步。代价是如果数据量极大每次onResume全量查询会有那么一点性能开销。但备忘录场景下用户笔记撑死几百条一次全量查询延迟在毫秒级别这点代价换来的是代码逻辑大幅简化划算。5. 源码包结构、集成方式与数据库升级5.1 源码包的完整结构说明整套源码包拿到手以后目录结构会是这样Notepad/ ├── README.md ├── app/ │ ├── build.gradle │ └── src/main/ │ ├── AndroidManifest.xml │ ├── java/com/example/notepad/ │ │ ├── activity/ ... │ │ ├── adapter/ ... │ │ ├── database/ ... │ │ ├── model/ ... │ │ └── utils/ ... │ └── res/ │ ├── layout/ ... │ ├── values/ │ └── drawable/ ... ├── build.gradle ├── gradle.properties └── gradle/wrapper/...初学者拿到项目后不要上来就盯着MainActivity一行行读我建议的阅读顺序是model/Note.java先看数据结构database/NoteDbHelper.java看表结构和升级逻辑database/NoteDao.java看SQL操作实现再到activity里面看调用关系。跟着数据流的走向去理解代码比按文件列表漫无目的地划拉效率高得多。README.md里我写了完整的运行环境要求、导入步骤、功能说明和常见问题包括Android Studio版本建议、模拟器设置、数据库文件存放位置等。这一块很多源码包都不重视但从实际反馈来看一个好README能挡掉至少七成的无效提问。5.2 把源码包功能集成进自己项目的两种方式很多朋友下载这套源码包不是为了直接用而是想移植到自己项目里。我把常用的两种集成方式都写在源码包的文档里。第一种是整工程替换如果你是新起步直接基于这套源码包改包名、改应用名、改logo然后把新增功能的代码添进去。改包名注意不要只改目录名要连Java文件顶部的package声明和import一起替换最稳妥的方式是用Android Studio的Refactor - Rename功能。第二种是拷贝核心代码到已有项目把model包、database包、utils包整体复制过去再复制note_item.xml和编辑页面布局最后在AndroidManifest里注册EditNoteActivity。这种方式的坑在于资源文件名不能冲突如果你原来的项目已经有note_item.xml就需要重命名。另外MyApplication这个类要不要带过去如果项目里没有自定义Application就把它带过去并在Manifest里声明如果已经有了把初始化的代码合并进去。无论哪种方式都强烈建议先把数据库表结构想清楚再改。源码包里的note表字段是比较普适的但如果你要加“收藏”字段、加“分类”字段最好在集成阶段就一起设计不要等上线了才通过onUpgrade打补丁。5.3 数据库升级策略与数据保全数据库升级是源码包里我专门写了一个示例的重要原因。上面NoteDbHelper的onUpgrade里example是新增category列。实际中的升级流程注意两点一定不要用drop table再重建。这样最简单但用户已有数据全没了。生产环境这么做会被骂到怀疑人生。正确的升级写法是先判断oldVersion小于对应版本就ALTER TABLE加列或者建新表做数据迁移。每次修改表结构都要把DB_VERSION 1。DB_VERSION不增加onUpgrade根本不会被触发改再多的表结构也没用这是无数人踩过的低级坑。数据保全的另外一个层面是备份。这套源码包没有做数据导出但我在README里提供了adb调试期间导出数据库的命令adb exec-out run-as com.example.notepad cat /data/data/com.example.notepad/databases/notepad.db notepad_backup.db调试阶段数据库一旦写坏恢复成本很低多备份一条路总归没错。6. 常见问题与排查实录6.1 SQLiteDatabase对象未关闭导致的泄漏这是所有SQLite初学者最容易踩的坑也是代码审查里必被抓的问题。此前有朋友拿他写的版本给我看主界面每次刷新都new一个新的NoteDao每个方法内部打开数据库后不关闭跑一个上午Logcat里开始疯狂输出leak日志后面直接数据库崩溃。正确的做法就是我前面说的让NoteDbHelper保持单例getWritableDatabase返回同一个SQLiteDatabase实例用完调用close()或依赖事务自动关闭。但这里有个细节如果你用单例Helper每次在Dao方法里主动close数据库也会有问题因为其他地方可能正在用同一个实例。推荐的写法是查询方法里的Cursor要确保关闭数据库本身的连接交给SQLiteOpenHelper统一管理实际项目中通常不需要频繁关闭。我写代码时给团队立过一个规矩谁写new SQLiteDatabase谁负责close如果用的是helper.getWritableDatabase就不要轻易调用close。这个约定到目前为止没出过大乱子。6.2 增删改后列表不刷新新手最常见的第二个问题数据库数据明明变了界面却还停留在旧状态。这个问题有75%出在“保存成功后直接按返回键而不重新查询数据”还有20%是“用了noteAdapter.notifyItemInserted但位置算错”剩下的5%是“数据更新到另一个数据库文件了”。这套源码包用onResume刷新的方案直接绕开了大部分刷新时机问题。需要强调的是如果你要扩展成搜索功能不能在onResume里直接全量刷新然后重新过滤那会很度消耗性能。搜索功能的正确姿势是单独写一个带关键词参数的查询方法比如public ListNote queryNotesByKeyword(String keyword) { SQLiteDatabase db helper.getReadableDatabase(); String where title LIKE ? OR content LIKE ?; String[] args {% keyword %, % keyword %}; return queryNoteList(db, where, args); }6.3 中文乱码与编码问题SQLite本身支持UTF-8Android的Java字符串默认也是UTF-16两者之间转换由系统自动处理正常场景没有乱码问题。真正导致乱码的往往出现在导出数据或外部编辑数据库文件时用Windows记事本打开db文件里的中文直接乱码用navicat等工具导入SQL文件时字符集没选UTF-8页面代码里没有统一设置。源码包里所有Activity和资源文件都明确指定UTF-8编码布局文件里设置EditText的inputType为text不做强限制。如果你在集成时遇到了乱码先检查IDE的文件编码设置其次是导出工具字符集代码里基本不会有大坑。6.4 数据库版本不一致引发崩溃一次实际反馈里用户下了源码包跑起来打开APP直接闪退Logcat显示no such column: category。排查后发现他之前跑过一版旧代码数据库文件已经存在表结构是旧的。新的代码把DB_VERSION升到了2理论上该走onUpgrade但实际情况是他的新代码里建表语句直接写了category字段旧的数据库文件里没有这一列而SQLiteOpenHelper认为版本号没变就不会升级。这个问题坑过很多人。我的建议是调试阶段如果数据库结构还在频繁调整可以直接卸载APP重装或者用adb uninstall com.example.notepad清掉数据等结构稳定了再统一走onUpgrade。生产环境则必须在发布前反复测试升级路径确保从老版本的任意状态都能平滑过渡绝不能指望用户卸载重装。写在最后的一点个人经验做完这套源码包再回头想记事本APP的难点从来不是那几行SQL而是数据库连接管理、界面刷新时机、空状态处理、升级策略这些边边角角。这些东西教程里很少详细讲但正是它们在决定一个项目能不能真正交付。如果你打算拿这套源码包做二次开发我会建议下一个版本优先做两件事一是给数据库加一个回收站表把删除操作改成软删除data_flag字段区分是否已删除这样用户体验直接上一个档次二是引入搜索功能把小规模的LIKE查询加上搜索框在现有界面里加一个入口非常容易。这两个方向都不需要我们大改架构卡在现有代码上扩展就行。我实际测试这套源码包时跑了旧手机和新手机两套测试环境数据库操作在几十条笔记量级下都是即点即响应完全不需要引入复杂的异步框架。如果将来笔记数量可能涨到几千甚至上万条则需要考虑把查询放到子线程或者用Room配合Flow。但那是后话基础扎实了迁移成本并不高。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑