☰
Android记事本开发:SQLite数据库实现与TaoToken统一Key接入实践
2026/10/11 20:05:12 网站建设 项目流程

1. 从零搭一个能落地的 Android 记事本:SQLite 建库与 AI 工具链统一配置

做 Android 记事本这类小工具,最容易被低估的就是数据层。界面画得快,真正决定 App 能不能长期用的是 SQLite 建库、增删改查封装和版本升级这几件事。我见过太多 Demo 把 SQL 语句直接写在 Activity 里,改个字段就要全局搜替换,最后连自己都不敢动。这篇就按一个真实记事本 App 的路径走:先用SQLiteOpenHelper把diary.db建起来,再把 CRUD 封装成独立适配器,最后把 AI 辅助编码工具的 endpoint 和 API Key 统一收到 TaoToken,避免 Cline、Claude Code、Codex 各配一套 Key 的重复劳动。

核心检索词先摆出来:Android 记事本 SQLite 数据库实现,本质是用SQLiteOpenHelper管理建库与版本,用SQLiteDatabase执行增删改查,用Cursor读取结果集。它适合谁?适合刚学完四大组件、想做一个完整可运行小项目的 Android 初学者,也适合想把 AI 编码工具链理顺的中级开发者。下面所有代码都可以直接复制进 Android Studio 跑,数据库文件落在/data/data/包名/databases/diary.db,用 Device File Explorer 就能看到。

2. 前置准备:TaoToken 统一 Key 与 Android 工程环境

2.1 为什么要把 AI 工具的 Key 统一到 TaoToken

写记事本的过程中,你大概率会同时开几个 AI 辅助工具:一个在 IDE 里补全 CRUD 代码,一个在终端里跑 Agent 改 bug,还有一个用来解释Cursor的移动逻辑。如果每个工具都单独填一家厂商的 Key,配置散落在settings.json、auth.json、环境变量里,换一次 Key 就要翻五个地方。TaoToken 的思路是提供一个统一的 OpenAI 兼容入口,把 Base URL 指向同一个地址,Key 也只维护一份。

官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数,配置时直接写这个根路径即可。

2.2 拿 Key 与确认模型 ID

登录后进入控制台,在 API Keys 页面创建一个新 Key,复制出来形如sk-开头的一串。这个 Key 就是后面所有工具共用的那一份。模型 ID 按你实际要用的填,比如对话类、代码类各有对应名称,配置时以控制台展示的为准,不要凭记忆写。

  • 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
  • API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
  • 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

2.3 Android 工程侧准备

新建一个 Empty Views Activity 工程,语言选 Java,minSdk 拉到 24 以上。在app/build.gradle里确认没有引入 Room 之类的 ORM,这篇要手写原生 SQLite,目的是让你看清每一层在干什么。建三个包:com.jk.service放数据库帮助类和适配器,com.jk放实体类,com.jk.ui放 Activity。

实体类Diary很简单,四个字段加构造和 getter/setter:

package com.jk; public class Diary { private Integer id; private String title; private String body; private String time; public Diary() {} public Diary(Integer id, String title, String body, String time) { this.id = id; this.title = title; this.body = body; this.time = time; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } }

这一步别偷懒,实体类字段和后面建表语句的列名要一一对应,否则getColumnIndex会返回 -1,读出来全是空值。

3. 可复制配置:SQLiteOpenHelper 建表与 CRUD 封装

3.1 建库与版本管理

SQLiteOpenHelper是 Android 提供的数据库管理基类,onCreate只在数据库第一次创建时调用,onUpgrade在版本号变大时调用。下面这份DbHelper直接可用,数据库名diary.db,版本 1:

package com.jk.service; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DbHelper extends SQLiteOpenHelper { public DbHelper(Context context) { super(context, "diary.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { String sql = "CREATE TABLE Diary (" + "diaryId integer primary key autoincrement, " + "title varchar(20), " + "body varchar(1000), " + "time varchar(40))"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级时在这里写迁移逻辑,比如加字段 // db.execSQL("ALTER TABLE Diary ADD body varchar(1000) null"); } }

注意onUpgrade里不要直接DROP TABLE再重建,那会把用户写的日记全清掉。正确做法是按oldVersion逐级迁移,比如从 1 升到 2 就加一列,从 2 升到 3 再加索引。

3.2 CRUD 适配器封装

把增删改查集中到DiaryAdapter,Activity 只调方法不碰 SQL。这里有个关键点:getWritableDatabase()和getReadableDatabase()在磁盘没满时返回的是同一个对象,但磁盘满时前者会抛异常,后者会降级为只读。写操作统一用getWritableDatabase(),读操作可以用getReadableDatabase()。

package com.jk.service; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.jk.Diary; import java.util.ArrayList; import java.util.List; public class DiaryAdapter { private DbHelper dbOpenHelper; public DiaryAdapter(Context context) { dbOpenHelper = new DbHelper(context); } public void save(Diary diary) { SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); String sql = "insert into Diary(title,body,time) values(?,?,?)"; db.execSQL(sql, new Object[]{diary.getTitle(), diary.getBody(), diary.getTime()}); db.close(); } public void update(Diary diary) { SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); String sql = "update Diary set title=?,body=? where diaryId=?"; db.execSQL(sql, new Object[]{diary.getTitle(), diary.getBody(), diary.getId()}); db.close(); } public void delete(Integer id) { SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); String sql = "delete from Diary where diaryId=?"; db.execSQL(sql, new Object[]{id}); db.close(); } public Diary find(Integer id) { SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); String sql = "select * from Diary where diaryId=?"; Cursor cursor = db.rawQuery(sql, new String[]{id.toString()}); if (cursor.moveToFirst()) { int diaryId = cursor.getInt(cursor.getColumnIndex("diaryId")); String title = cursor.getString(cursor.getColumnIndex("title")); String body = cursor.getString(cursor.getColumnIndex("body")); String time = cursor.getString(cursor.getColumnIndex("time")); cursor.close(); db.close(); return new Diary(diaryId, title, body, time); } cursor.close(); db.close(); return null; } public List<Diary> getScrollData(int offset, int maxResult) { List<Diary> list = new ArrayList<>(); SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); String sql = "select * from Diary limit ?,?"; Cursor cursor = db.rawQuery(sql, new String[]{ String.valueOf(offset), String.valueOf(maxResult)}); while (cursor.moveToNext()) { int diaryId = cursor.getInt(cursor.getColumnIndex("diaryId")); String title = cursor.getString(cursor.getColumnIndex("title")); String body = cursor.getString(cursor.getColumnIndex("body")); String time = cursor.getString(cursor.getColumnIndex("time")); list.add(new Diary(diaryId, title, body, time)); } cursor.close(); db.close(); return list; } public long getCount() { SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("select count(*) from Diary", null); cursor.moveToFirst(); long count = cursor.getLong(0); cursor.close(); db.close(); return count; } }

用?占位符而不是字符串拼接,是为了防止 SQL 注入,也避免标题里带单引号时语句被截断。getScrollData里的limit ?,?第一个参数是偏移量,第二个是每页条数,做分页列表时直接传page * pageSize和pageSize。

3.3 AI 工具统一 Key 的配置片段

现在把 AI 编码工具的配置也统一到 TaoToken。以 Cline 的 MCP 配置为例,在cline_mcp_settings.json里写:

{ "mcpServers": { "taotoken": { "command": "npx", "args": ["-y", "@taotoken/mcp-server"], "env": { "OPENAI_BASE_URL": "https://taotoken.net/api", "OPENAI_API_KEY": "sk-你的Key", "OPENAI_MODEL": "你的模型ID" } } } }

如果你用的是 Claude Code,配置走settings.json,把 Base URL 和 Key 填进对应字段;Codex 则改auth.json。三件套永远是 Base URL、Key、Model ID,缺一个都连不上。Base URL 写https://taotoken.net/api,不要多加/v1之类的后缀,具体以接入文档为准。

4. 验证请求:跑通数据库与 AI 连通性

4.1 验证 SQLite 是否真的写进去了

在 Activity 里调一次保存,再查总数:

DiaryAdapter adapter = new DiaryAdapter(this); Diary d = new Diary(); d.setTitle("第一篇"); d.setBody("今天开始用 SQLite 存日记"); d.setTime("2025-01-01 10:00"); adapter.save(d); long count = adapter.getCount(); android.util.Log.d("DiaryTest", "当前记录数=" + count);

运行后看 Logcat,输出当前记录数=1就说明建库、建表、插入、查询整条链路通了。再用 Android Studio 的 Device File Explorer 进/data/data/你的包名/databases/,能看到diary.db和diary.db-journal,说明文件确实落盘了。

4.2 验证 TaoToken 连通性

用 curl 发一个最小请求,确认 Key 和地址没问题:

curl https://taotoken.net/api/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "你的模型ID", "messages": [{"role": "user", "content": "只回复 ok"}] }'

返回体里choices[0].message.content有内容,就说明统一 Key 生效了。这一步过了,再把同样的 Base URL 和 Key 填进 Cline、Claude Code,就不用重复试错了。

5. 本篇常见报错排查

5.1 401 Unauthorized

最常见的原因是 Key 复制时带了空格,或者把Bearer前缀漏了。检查Authorization头是不是Bearer sk-xxx格式。还有一种情况是 Key 被禁用或额度用尽,去控制台 API Keys 页面确认状态。

5.2 local proxy failed

这个报错通常出现在 AI 工具侧,说明工具尝试走本地代理但没起来。检查你的工具配置里有没有多余的proxy字段,把它删掉,让请求直连https://taotoken.net/api。同时确认 Base URL 没有写成http://localhost:xxxx这种本地地址。

5.3 reading choices 相关报错

返回体解析失败,报reading 'choices'之类,多半是响应不是标准 OpenAI 格式。先确认请求打到了正确的 endpoint,路径是/api/chat/completions。如果模型 ID 写错,服务端可能返回错误结构,工具解析时就崩了。用第 4.2 节的 curl 先验证一遍,能拿到标准响应再回工具里配。

5.4 OAuth 相关报错

有些工具默认走 OAuth 登录流程,报OAuth token expired或类似信息。这类工具要切到 API Key 模式,在配置里显式指定OPENAI_API_KEY,不要让它去读本地缓存的登录态。Claude Code 和 Codex 都有对应的 Key 模式开关,按接入文档改。

5.5 SQLite 侧常见坑

getColumnIndex返回 -1 导致读空值,检查列名大小写是否和建表语句一致。Cursor用完不close会泄漏,每个查询方法里都要关。db.close()调太早会导致后续操作报attempt to re-open an already-closed object,确保在 Cursor 读完之后再关。

6. 把工具链收拢到一处

数据库这层跑通之后,记事本的骨架就立住了。接下来加列表页、编辑页、时间格式化,都是在这个 CRUD 封装上叠 UI。AI 工具那边,把 Cline、Claude Code、Codex 的 Base URL 和 Key 都指向 TaoToken 同一份配置,换 Key 时只改一处,省下的时间够你多写两个功能。

需要长期跑编码 Agent 的话,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先验证模型对话效果,用模型对话页:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入细节以文档为准:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Key 在 API Keys 页面管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

最后留一个实操建议:onUpgrade里写迁移逻辑时,先在测试机上把版本号从 1 改到 2 跑一遍,确认老数据还在,再提交。数据库这东西,改错了比 UI 崩了难恢复得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询