数据结构与本地数据库 04:从打包 JSON 读取知识卡片

课程首页 · 全部知识课程 · 返回专题主页

本系列第 4/8 课,建议 45–60 分钟(估算)。先完成上一课。

下载本课完成工程 .aia

这课解决什么

资源文件随 .aia 和 APK 一起打包。File.ReadFrom 的 // 前缀表示资产,JSON 顶层必须是列表,每项必须是包含题目和答案的字典。网络客户端在此只负责 JSON 解码,没有联网请求。

先预测,再运行

//data/cards.json 是手机共享目录里的文件吗?

先记下自己的答案,运行起始工程与完成工程比较;答案见文末。

开始实战

以上一课完成工程为起点,先导入并运行,再另存为本课作业。

阶段工程各自保存数据;导入下一课不会自动带走手机上的旧数据。练习使用本课示例数据,迁移方法在数据课程第六课单独讲解。

新增或调整的组件

实例名 组件 本课配置
标签_标题 标签 Width=Fill parent;FontSize=24;TextColor=#183153;Text=知识卡片 · 第 4课
微数据库1 微数据库 Namespace=CardsB04
文本输入框_导入文件 文本输入框 Hint=资源文件名或已导出的 JSON;Width=Fill parent;FontSize=16;Text=//data/cards.json
按钮_导入 按钮 Width=Fill parent;BackgroundColor=#2563EB;TextColor=#FFFFFF;FontSize=16;Text=导入
文件管理器1 文件管理器 沿用默认值
Web客户端1 Web客户端 沿用默认值

逐步搭建

  1. 查看工程资源里的 data/cards.json,标出列表、字典和字段层级。
  2. 增加文件管理器和 Web 客户端,导入按钮调用 ReadFrom,等待 GotText 再解码。
  3. 检查顶层列表和每项字典;缺题目或答案的记录跳过,并报告导入数、跳过数。
  4. 分别导入 cards.json、invalid.json、missing.json、broken.json 和不存在路径;比较提示。

知识卡片与错题库第4课运行状态布局预览

图来自本课最终工程执行积木后的状态,为 HTML 近似预览;不是手机截图。

核心积木与数据流

下面是本课完成工程中的核心代码,包含所需全局变量和过程。可在块图中追踪数值,再回到工程对照完整事件;合并到已有工程时,同名事件合并处理。

global 卡片 = list(makedict(pair("id", 1), pair("question", "中国历史朝代顺序从哪里查?"), pair("answer", "按资料的年代字段排序"), pair("category", "历史"), pair("wrong", 0)), makedict(pair("id", 2), pair("question", "列表第一个索引是多少?"), pair("answer", "1"), pair("category", "编程"), pair("wrong", 0)))
global 显示 = []
global 选中ID = 0
global 下个ID = 3
global 关键词 = ""

procedure 刷新() {
显示 = []
var 标题 = []
foreach 项 in 卡片 {
if 关键词 = "" or textContains(lower(join(dictget(项,"question",""), " ", dictget(项,"category",""))), lower(关键词)) {
listadd(显示, 项)
listadd(标题, join("#", dictget(项,"id",0), " · ", dictget(项,"category",""), " · ", dictget(项,"question","")))
}
}
列表显示框_卡片.Elements = 标题
选中ID = 0
标签_详情.Text = "点击卡片查看详情"
标签_状态.Text = join("显示 ", length(显示), " / ", length(卡片), " 张卡片")
}

procedure 保存() {
微数据库1.StoreValue("卡片", 卡片)
微数据库1.StoreValue("下个ID", 下个ID)
刷新()
}

when 文件管理器1.GotText(text) {
var 数据 = Web客户端1.JsonTextDecodeWithDictionaries(text)
if islist(数据) {
var 导入数 = 0
var 跳过数 = 0
foreach 项 in 数据 {
if isdict(项) {
var 问题 = dictget(项,"question","")
var 答案 = dictget(项,"answer","")
var 分类 = dictget(项,"category","未分类")
var 重复 = false
if trim(问题) != "" and trim(答案) != "" and not 重复 {
listadd(卡片,makedict(pair("id",下个ID),pair("question",问题),pair("answer",答案),pair("category",分类),pair("wrong",0)))
下个ID = 下个ID + 1
导入数 = 导入数 + 1
} else { 跳过数 = 跳过数 + 1 }
} else { 跳过数 = 跳过数 + 1 }
}
保存()
标签_状态.Text = join("导入 ", 导入数, " 张,跳过 ", 跳过数, " 张")
} else { 标签_状态.Text = "JSON 顶层必须是列表" }
}

中途检查与排错

本课以下控制与数据流已执行最终归档中的真实积木检查。请在伴侣或安装的 APK 中按相同场景操作,记录实际结果;自动检查通过不代表手机已实测。

  1. 按索引读取详情。
  2. 空题不保存。
  3. 含引号逗号换行的记录保存。
  4. 重启完整恢复。
  5. 取消删除保留。
  6. 确认删除正确对象。
  7. 实际读取嵌套asset JSON。
  8. 错误顶层、缺字段、坏JSON与资源不存在拒绝。

排查时先确定出错的是输入、状态、数据读取还是显示;用标签暂时显示关键值,每次只改变一个条件。

当前课仅实现表中对应功能,导出和备份在第八课加入。SQLite 在第六课加入,本课仍使用列表或 TinyDB。

两个理解题与独立改造

  1. //data/cards.json 是手机共享目录里的文件吗?
  2. 把 JSON 当普通文本保存,与解码为列表有什么区别?

独立改造: 准备三张自己写的历史卡片,放在 data/my-cards.json 并导入。

先完成自己的工程,再对照答案。检查成功路径,也检查空数据、重复操作和取消;写出你改动的规则。

查看预测与练习参考答案

预测: 不是,它是工程资产 data/cards.json。伴侣和 APK 读取资产的实现不同,但使用相同资源名。

追踪: 文本可以显示,但不能直接按列表或字典操作;解码恢复结构及类型。

改造思路: 顶层使用 […],每项包含 question、answer、category;在文本输入框填 //data/my-cards.json。说明答案来源,不能凭空编造历史事实。

完成与下一步

保存自己的工程并导出 .aia;给文件命名以区分本课作业和参考源码。请用自己的话解释本课知识,并展示一次失败输入的处理。

上一课 · 下一课:搜索、倒序与重复导入

文档反馈