ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

NotePad 笔记本应用功能拓展:用 SQLite 给每条笔记加上时间戳与关键字查询

NotePad 笔记本应用功能拓展:用 SQLite 给每条笔记加上时间戳与关键字查询 1. 从 NotePad 到「带时间戳 可搜索」的本地笔记问题到底出在哪如果你手上有 Android 官方 NotePad 示例工程或者自己照着写过一版大概率会遇到两个很具体的体验问题列表里只有标题看不出这条笔记是刚记的还是上周写的笔记一多想找某条内容只能靠手指一条条往下翻。这两个问题本质上都落在同一个地方——本地数据层没有把「时间」和「查询」当回事。NotePad 默认的noteslist_item.xml只放了一个TextView显示标题NoteList里的PROJECTION也只取了_ID和COLUMN_NAME_TITLE。数据库里其实早就定义了COLUMN_NAME_CREATE_DATE和COLUMN_NAME_MODIFICATION_DATE两个字段但插入时没写值列表里也没读出来等于白留了两个列。查询功能更是完全没有菜单里连个搜索入口都没有。这篇要解决的就是这两件事给每条笔记补上创建时间和修改时间并在列表项下方显示出来再加一个基于SearchView的查询页按标题或内容做模糊匹配。适合已经跑通 NotePad 基础增删改、想继续把本地数据层做扎实的 Android 初学者也适合在维护老示例工程、需要快速补功能的同学。我试过在真机上直接改这套代码最容易踩的坑不是 SQL 写错而是时间格式化时区没设对导致显示出来的时间和手机系统时间差 8 小时另一个坑是SimpleCursorAdapter的from数组和to数组长度不一致直接崩在IllegalArgumentException。下面按「建表升级 → 写入时间戳 → 列表显示 → SearchView 查询 → 排障」的顺序走一遍代码都能直接复制。核心检索词先明确NotePad 时间戳显示、SQLite 建表升级、SearchView 关键字查询、SimpleCursorAdapter 多列绑定。这几个词会贯穿全文你按这个思路改完列表能显示时间、搜索能按标题和内容过滤。2. 前置准备SQLite 表结构、字段与 NotePadProvider 改造点动手之前先把数据层看清楚。NotePad 的NotePadProvider里有一个DatabaseHelperonCreate里执行建表语句onUpgrade里做版本迁移。默认建表大致是这样// NotePadProvider.java 中的建表语句默认版本 private static final String CREATE_TABLE CREATE TABLE NotePad.Notes.TABLE_NAME ( NotePad.Notes._ID INTEGER PRIMARY KEY, NotePad.Notes.COLUMN_NAME_TITLE TEXT, NotePad.Notes.COLUMN_NAME_NOTE TEXT, NotePad.Notes.COLUMN_NAME_CREATE_DATE INTEGER, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE INTEGER );;注意这里两个日期字段的类型是INTEGER官方示例原本打算存毫秒时间戳。但很多教程包括你手上这份 excerpt改成了存格式化字符串yyyy/MM/dd HH:mm:ss然后字段类型还是INTEGER。SQLite 是弱类型存字符串进去不会报错但排序会按字符串字典序走2024/09/01和2024/10/01比较时没问题跨年也还行因为格式固定。真正的问题是如果你后面想按时间范围查询字符串比较就不如整数时间戳灵活。我的建议是分两种情况处理方案字段类型存储内容优点缺点A 字符串格式化TEXT2024/09/23 14:30:00列表直接显示不用再格式化范围查询、排序不够灵活B 毫秒时间戳INTEGER1727073000000排序、范围查询方便显示前要再格式化一次如果你只是想让列表显示时间、搜索按关键字过滤方案 A 最省事和 excerpt 的思路一致。如果你后面还想做「按时间倒序」「查最近七天」建议用方案 B显示时再格式化。下面我按方案 A 写因为改动最小同时把方案 B 的兼容写法也标出来。数据库版本号要改。DatabaseHelper里通常有private static final int DATABASE_VERSION 1;如果你已经装过旧版本 App直接改字段类型不会触发onUpgrade得把版本号加一并在onUpgrade里补上迁移逻辑。全新安装的话onCreate直接建新表即可。// 升级版本号 private static final int DATABASE_VERSION 2; Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 简单粗暴旧版本直接重建表会丢数据仅示例用 // 生产环境请用 ALTER TABLE 或数据迁移 if (oldVersion 2) { db.execSQL(DROP TABLE IF EXISTS NotePad.Notes.TABLE_NAME); onCreate(db); } }注意DROP TABLE会清空已有笔记。如果你不想丢数据用ALTER TABLE notes ADD COLUMN ...逐列加或者把旧数据读出来再写回新表。示例工程里为了演示方便才直接重建。字段常量确认一下NotePad.Notes里应该有public static final String COLUMN_NAME_CREATE_DATE created; public static final String COLUMN_NAME_MODIFICATION_DATE modified;如果你的工程里字段名不一样后面所有 SQL 和PROJECTION都要跟着改别照抄字段名。3. 可复制配置时间戳写入、列表绑定与 SearchView 查询回调这一节是核心分三块写入时间戳、列表显示时间、SearchView 查询。每块都给完整代码。3.1 插入和更新时写入时间戳在NotePadProvider的insert和update方法里补上时间字段。关键是时区要设成GMT08:00否则真机上可能显示 UTC 时间。// NotePadProvider.java 的 insert 方法内 Override public Uri insert(Uri uri, ContentValues values) { // ... 前面的 uri 匹配逻辑 // 获取当前时间格式化为北京时间 Long now Long.valueOf(System.currentTimeMillis()); Date date new Date(now); SimpleDateFormat format new SimpleDateFormat(yyyy/MM/dd HH:mm:ss); format.setTimeZone(TimeZone.getTimeZone(GMT08:00)); String formatDate format.format(date); // 如果调用方没有传创建时间就补上 if (values.containsKey(NotePad.Notes.COLUMN_NAME_CREATE_DATE) false) { values.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, formatDate); } // 修改时间同理 if (values.containsKey(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE) false) { values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, formatDate); } // ... 继续执行 db.insert }update方法里只需要补修改时间创建时间不动Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { // 每次更新都刷新修改时间 Long now Long.valueOf(System.currentTimeMillis()); Date date new Date(now); SimpleDateFormat format new SimpleDateFormat(yyyy/MM/dd HH:mm:ss); format.setTimeZone(TimeZone.getTimeZone(GMT08:00)); values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, format.format(date)); // ... 继续执行 db.update }如果你用方案 B存毫秒把上面formatDate换成String.valueOf(now)字段类型改成INTEGER显示时再格式化。3.2 列表项布局加时间 TextViewnoteslist_item.xml里加一个TextView放在标题下面LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding8dp TextView android:idandroid:id/text1 android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize18sp / TextView android:idid/text1_date android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize14sp android:paddingLeft5dip android:textColor#888888 / /LinearLayout注意标题的 id 用的是android:id/text1因为SimpleCursorAdapter默认绑到android.R.id.text1。时间用自定义 idtext1_date。3.3 NoteList 里绑定两列数据NoteList.java的PROJECTION要加上修改时间列from和to数组要一一对应private static final String[] PROJECTION new String[] { NotePad.Notes._ID, // 0 NotePad.Notes.COLUMN_NAME_TITLE, // 1 NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE // 2 }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); Intent intent getIntent(); if (intent.getData() null) { intent.setData(NotePad.Notes.CONTENT_URI); } getListView().setOnCreateContextMenuListener(this); Cursor cursor managedQuery( getIntent().getData(), PROJECTION, null, null, NotePad.Notes.DEFAULT_SORT_ORDER ); String[] dataColumns { NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE }; int[] viewIDs { android.R.id.text1, R.id.text1_date }; SimpleCursorAdapter adapter new SimpleCursorAdapter( this, R.layout.noteslist_item, cursor, dataColumns, viewIDs ); setListAdapter(adapter); }dataColumns和viewIDs长度必须一致顺序也要对应标题绑text1时间绑text1_date。少一个就崩。3.4 菜单加搜索入口在list_options_menu.xml里加一项item android:idid/menu_search android:iconandroid:drawable/ic_menu_search android:titleSearch app:showAsActionalways /然后在NoteList的onOptionsItemSelected里加 casecase R.id.menu_search: Intent searchIntent new Intent(this, NoteSearch.class); this.startActivity(searchIntent); return true;3.5 NoteSearch 布局与查询回调新建NoteSearch.java和note_search.xml。布局?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical SearchView android:idid/search_view android:layout_widthmatch_parent android:layout_heightwrap_content android:iconifiedByDefaultfalse / ListView android:idid/list_view android:layout_widthmatch_parent android:layout_heightwrap_content / /LinearLayoutNoteSearch.java完整实现public class NoteSearch extends Activity implements SearchView.OnQueryTextListener { ListView listView; SQLiteDatabase sqLiteDatabase; SearchView searchView; private static final String[] PROJECTION new String[] { NotePad.Notes._ID, NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_search); searchView findViewById(R.id.search_view); listView findViewById(R.id.list_view); sqLiteDatabase new NotePadProvider.DatabaseHelper(this).getReadableDatabase(); searchView.setOnQueryTextListener(this); } Override public boolean onQueryTextSubmit(String query) { return false; } Override public boolean onQueryTextChange(String newText) { String selection NotePad.Notes.COLUMN_NAME_TITLE LIKE ? OR NotePad.Notes.COLUMN_NAME_NOTE LIKE ?; String[] selectionArgs { % newText %, % newText % }; Cursor cursor sqLiteDatabase.query( NotePad.Notes.TABLE_NAME, PROJECTION, selection, selectionArgs, null, null, NotePad.Notes.DEFAULT_SORT_ORDER ); String[] dataColumns { NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE }; int[] viewIDs { android.R.id.text1, R.id.text1_date }; SimpleCursorAdapter adapter new SimpleCursorAdapter( this, R.layout.noteslist_item, cursor, dataColumns, viewIDs ); listView.setAdapter(adapter); return true; } }这里onQueryTextChange每输入一个字符就查一次适合笔记量不大的场景。如果笔记上千条建议改成onQueryTextSubmit里查或者加 300ms 防抖。4. 验证请求插入、更新、模糊查询三步实测代码写完别急着说「应该没问题」按下面三步在真机或模拟器上跑一遍每步都有明确的预期结果。第一步插入一条新笔记验证创建时间和修改时间都写进去了。打开 App点菜单新建笔记标题填「测试时间戳」内容随便写。保存后回到列表应该看到这条笔记下面显示类似2024/09/23 14:30:00的时间。如果显示空白说明PROJECTION没取到时间列或者SimpleCursorAdapter的viewIDs没绑对。想直接看数据库的话用 Android Studio 的 App Inspection → Database Inspector打开notes表看created和modified两列是不是都有值。命令行方式adb shell run-as 你的包名 cd databases sqlite3 note_pad.db SELECT _id, title, created, modified FROM notes;预期输出类似1|测试时间戳|2024/09/23 14:30:00|2024/09/23 14:30:00第二步编辑这条笔记验证修改时间刷新、创建时间不变。点进笔记改一下内容再保存回到列表时间应该变成当前时间。再查数据库1|测试时间戳改|2024/09/23 14:30:00|2024/09/23 14:35:12created保持原值modified更新说明update方法里的时间写入生效了。如果两个都变了检查是不是在update里误写了COLUMN_NAME_CREATE_DATE。第三步搜索关键字验证标题和内容都能匹配。再建两条笔记一条标题「购物清单」内容「牛奶 鸡蛋」一条标题「工作记录」内容「购物清单已整理」。打开搜索页输入「购物」预期两条都出现——一条是标题命中一条是内容命中。输入「牛奶」只出现第一条。输入「xyz」列表为空。如果搜索时列表不刷新检查onQueryTextChange有没有return true以及listView.setAdapter有没有在每次查询后调用。如果一输入就崩看 Logcat 是不是IllegalArgumentException: column _id does not exist那是PROJECTION里漏了_IDSimpleCursorAdapter强制要求游标里有_id列。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 之外的本地坑这一节把改 NotePad 时最容易撞上的报错和现象列出来对照 Logcat 定位。报错一java.lang.IllegalArgumentException: column _id does not exist原因PROJECTION数组里没包含NotePad.Notes._ID。SimpleCursorAdapter内部要求游标必须有_id列否则直接抛异常。解决确保PROJECTION第一项是NotePad.Notes._ID查询时也带上。报错二java.lang.IllegalArgumentException: from[] and to[] must be the same length原因dataColumns和viewIDs长度不一致。比如dataColumns有 2 个viewIDs只写了 1 个。解决一一对应标题对android.R.id.text1时间对R.id.text1_date。报错三时间显示差 8 小时原因SimpleDateFormat没设时区默认用 UTC。解决format.setTimeZone(TimeZone.getTimeZone(GMT08:00))。注意GMT08:00写法别写成GMT8部分机型解析不一致。报错四搜索时no such column: note原因NotePad.Notes.COLUMN_NAME_NOTE常量值和数据库实际列名不一致。默认是note如果你改过建表语句要同步改常量。解决查NotePad.Notes里的常量定义和CREATE TABLE里的列名对齐。报错五onUpgrade没触发新字段没加上原因DATABASE_VERSION没改或者 App 没卸载重装。解决版本号加一卸载旧 App 重装或者手动在onUpgrade里加ALTER TABLE。报错六搜索页NullPointerException在sqLiteDatabase.query原因DatabaseHelper初始化失败或者getReadableDatabase()返回 null。解决确认NotePadProvider.DatabaseHelper的构造函数能正常拿到 Context别在onCreate之前调用。报错七local proxy failed/reading choices/401/OAuth这几个通常出现在你后续想把笔记数据同步到云端、或者接入大模型做摘要时。本地 SQLite 改造本身不会产生这些错误。如果你在接入外部 API 时遇到401先检查 Key 是否有效、请求头Authorization格式对不对遇到local proxy failed检查本地网络配置和 Base URL 是否可达遇到reading choices多半是响应体解析时字段名对不上遇到OAuth相关报错检查回调地址和 token 是否过期。这些和本篇的 SQLite 时间戳、SearchView 查询是两回事别混在一起排查。排查顺序建议先看 Logcat 第一条异常定位到具体行号再用 Database Inspector 看数据有没有写进去最后用adb shell直接跑 SQL 验证查询条件。三步下来基本能锁定问题。6. 把本地数据层做扎实之后还能往哪走时间戳和关键字查询只是 NotePad 本地数据层的最小闭环。改完这套之后你会发现几个自然的延伸点列表按修改时间倒序排列只需要把DEFAULT_SORT_ORDER改成COLUMN_NAME_MODIFICATION_DATE DESC搜索加防抖在onQueryTextChange里用Handler.postDelayed延迟 300ms 再查时间显示改成「刚刚」「5 分钟前」这种相对格式写个工具方法把字符串解析回Date再算差值。如果你后面想让笔记支持云同步、或者接入模型做自动摘要和标签本地这层的时间戳和查询就是基础。同步时需要知道哪些笔记改过靠的就是modified字段做增量拉取时按modified 上次同步时间过滤即可。接入外部服务时Base URL、Key、Model ID 三件套要配齐请求头发Authorization: Bearer 你的Key响应体解析注意字段名和类型。这些属于另一条链路等本地数据层稳定了再往上叠。回到眼前最实用的建议是先把DATABASE_VERSION和onUpgrade处理好别等线上有数据了才想起迁移再把PROJECTION、dataColumns、viewIDs三处对齐能省掉大部分崩溃。剩下的就是多插几条笔记、多搜几次用真实数据把边界情况跑出来。
返回列表