MicroReader已读标记功能实现:用SQLite高效记录阅读历史的技巧
【免费下载链接】MicroReader一个小而美的阅读客户端项目地址: https://gitcode.com/gh_mirrors/mi/MicroReader
MicroReader(微阅)是一款小而美的 Android 阅读客户端,聚合了微信精选、IT之家、果壳热门、知乎日报和视频推荐五大栏目。今天我们就来拆解它的已读标记功能:如何用SQLite高效记录阅读历史,让已读文章标题变灰、未读文章保持黑色,并且在大量数据下依旧流畅不卡顿。这套实现思路对任何新闻类、资讯类 App 都有直接参考价值。
为什么需要已读标记功能?📌
用户每天面对几十上百条资讯,如果分不清哪些看过、哪些没看过,阅读体验会大打折扣。MicroReader 的做法非常直观:
| 状态 | 标题颜色 | 说明 |
|---|---|---|
| 未读 | 黑色 | 新文章、待阅读 |
| 已读 | 灰色 | 已点开过的文章 |
实现要点有两个:一是数据层面要可靠记录"哪篇文章读过",二是界面层面要随状态实时刷新颜色。MicroReader 用 SQLite 数据库完成前者,用 Adapter 的onBindViewHolder完成后者。
用 SQLite 表结构设计记录阅读历史
MicroReader 把已读记录单独存放在IsRead.db数据库中,每个栏目一张表(微信、果壳、知乎、视频、IT),表结构统一且极其精简。核心代码见 DBUtils.java:
create table if not exists 表名 ( id integer primary key autoincrement, key text unique, is_read integer )只有三个字段:
- id:自增主键,用于删除最旧记录
- key:文章唯一标识(如新闻 id 或 URL),加了
unique约束防止重复 - is_read:状态值,
1表示已读,0表示未读
表名在 Config.java 中统一定义,数据库初始化时在 DBUtils.java 的 onCreate 里一次性创建五张表,代码非常干净。
单例封装:全局只用一个数据库连接
数据库操作类最容易犯的错就是频繁开关连接导致性能下降。MicroReader 使用双重校验的单例模式,整个 App 生命周期内只有一个SQLiteDatabase实例,见 DBUtils.java:
public static synchronized DBUtils getDB(Context context) { if (sDBUtis == null) sDBUtis = new DBUtils(context); return sDBUtis; }之后任何界面要查询、写入已读状态,只需一行:DBUtils.getDB(mContext),无需关心连接管理,这也是它能在列表滚动时依然流畅的关键原因。
高效写入已读记录的两个技巧
技巧一:用 CONFLICT_REPLACE 告别重复数据
插入已读记录时,MicroReader 没有先查再插的冗余逻辑,而是直接用insertWithOnConflict配合CONFLICT_REPLACE,见 DBUtils.java:
mSQLiteDatabase.insertWithOnConflict(table, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);当key已存在时直接覆盖更新,不存在则新增。一条语句同时搞定"插入"和"更新",既快又不会产生重复记录。
技巧二:超过 200 条自动清理,防止数据库无限膨胀
阅读记录会随着使用越积越多,如果不清理,数据库迟早拖慢查询。MicroReader 的做法很实用:每次写入前检查记录数,超过 200 条就删除 id 最小(最旧)的一条,见 DBUtils.java:
if (cursor.getCount() > 200 && cursor.moveToNext()) { mSQLiteDatabase.delete(table, "id=?", new String[]{ String.valueOf(cursor.getInt(cursor.getColumnIndex("id")))}); }这样数据库永远只保留最近 200 条记录,查询和写入都保持 O(1) 级别的开销,长期使用也不会变慢。
快速查询:一条 SQL 判断已读状态
查询逻辑同样轻量,按key精确匹配并比对is_read值,见 DBUtils.java:
public boolean isRead(String table, String key, int value) { Cursor cursor = mSQLiteDatabase.query(table, null, "key=?", new String[]{key}, null, null, null); if (cursor.moveToNext() && cursor.getInt(cursor.getColumnIndex("is_read")) == value) { isRead = true; } cursor.close(); return isRead; }这里有个小细节值得学习:isRead方法接收一个value参数,默认传1表示"查询是否已读",也可以传0表示"查询是否未读",一个方法兼顾两种需求,设计很灵活。
在 RecyclerView 中呈现已读状态
数据层就绪后,界面呈现就简单了。以果壳栏目为例,Adapter 在 GuokrAdapter.java 中绑定数据时查询数据库并设置颜色:
if (DBUtils.getDB(mContext).isRead(Config.GUOKR, guokrHotItem.getId(), 1)) holder.mTvTitle.setTextColor(Color.GRAY); else holder.mTvTitle.setTextColor(Color.BLACK);用户点击文章时,先写入已读记录,再把标题立即变灰,见 GuokrAdapter.java:
DBUtils.getDB(mContext).insertHasRead(Config.GUOKR, guokrHotItem.getId(), 1); holder.mTvTitle.setTextColor(Color.GRAY);值得一提的是,微信和知乎栏目还支持手动切换已读/未读状态:长按弹出菜单,通过写入1或0实现双向标记,见 WeixinAdapter.java 和 ZhihuAdapter.java。这一设计在资讯类 App 中相当贴心——用户可能误点文章,或想重新阅读,都能一键恢复。
写在最后:这套方案的三个借鉴点 ✅
- 表结构极简:只用
key + is_read两个业务字段,配合unique约束天然去重,任何栏目都能复用同一套建表语句。 - 性能优先:单例数据库连接 + 冲突替换写入 + 200 条上限清理,从源头上避免了数据库膨胀和连接泄漏。
- 读写分离清晰:DBUtils 只负责存取,Adapter 只负责展示,职责单一,扩展新栏目时只需在 Config 中加一个表名常量。
如果你想动手实践,可以 clone 项目到本地学习完整源码:
https://gitcode.com/gh_mirrors/mi/MicroReader重点阅读 DBUtils.java、Config.java 以及各栏目 Adapter 的已读逻辑部分。理解这套 SQLite 已读标记方案后,你完全可以在自己的阅读类 App 中复刻同样的体验——从记录阅读历史到标题变灰,仅需几十行代码,却能极大提升产品的细节质感。
【免费下载链接】MicroReader一个小而美的阅读客户端项目地址: https://gitcode.com/gh_mirrors/mi/MicroReader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考