Files
App.K12.Student.Miniapp_Flu…/docs/demos/exam_demo.md
T
2026-09-04 15:22:20 +08:00

110 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】试卷练习入口(大数据滑动验证页)
> 源文件:`lib/pages/demo/exam_demo.dart` | 分类:考试(exam)
> 一句话:试卷练习的入口页 —— 选题目数量(50/100/200)→ 进入真实考试页,验证 PageView 大数据滑动不卡。
---
## 一、这个 Demo 解决什么问题
真实考试页(`lib/features/exam/exam_page.dart`)是**本项目的核心功能**,完整架构见
[exam_architecture.md](../exam_architecture.md)(建议先读)。本页是它的**演示入口**,回答三个问题:
1. **100 道题滑动卡不卡?** —— PageView.builder 懒加载:只构建视口 ±1 页,滑出的页面自动回收;
2. **四种题型怎么共存?** —— 按 `question.type` switch 分发到单选/多选/判断/解答卡片;
3. **答一半退出,进度还在吗?** —— Riverpod(内存主状态)+ Isar(AnswerRecord 备份层),重进自动恢复。
> 对照 Vue:对标 swiper 大数据瓶颈——Vue 会把所有 DOM 挂上,Flutter 的 PageView.builder 只构建看得见的 ±1 页。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: ExamDemoPage()));
```
1. **SegmentedButton 选数量**(50/100/200 题)→ 点"开始练习";
2. 快速左右滑动感受流畅度(`allowImplicitScrolling` 预构建相邻页);
3. 答几题 → 退出 → 重新进入 → SnackBar 提示"已恢复上次答题进度(X 题),定位到第 Y 题";
4. 交卷 → 自动判分(客观题)+ 按 paperId 清理本地进度。
---
## 三、核心知识点(新手向)
### 1. 数量选择 = Riverpod 全局状态
```dart
class ExamDemoPage extends ConsumerWidget { // ① 用 ConsumerWidget 才能 ref
...
final size = ref.watch(paperSizeProvider); // ② watch:变化自动重建
SegmentedButton<int>(
segments: [for (final s in _sizes) ButtonSegment(value: s, label: Text('$s 题'))],
selected: {size},
onSelectionChanged: (selection) {
ref.read(paperSizeProvider.notifier).setSize(selection.first); // ③ 回调里 read
},
)
```
- **build 里 `ref.watch`**(响应式重建),**回调里 `ref.read`**(只读一次不订阅)——Riverpod 的黄金规则;
- `paperSizeProvider` 是全局的:入口页选的 50 题,考试页读到的就是 50 题(跨页共享状态)。
### 2. 进入考试页 = 普通跳转
```dart
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ExamPage()),
);
```
考试页通过同一个 `paperSizeProvider` 拿题量——**传参交给状态管理,路由只负责跳转**(对比 [router_home_page.md](router_home_page.md) 的构造传参)。
### 3. 三个架构要点(页面里直接写了卡片)
| 要点 | 说明 |
|---|---|
| PageView.builder 懒加载 | 只构建视口 ±1 页,滑出即回收;`allowImplicitScrolling` 预构建相邻页 → 100 题滑动不掉帧 |
| 题型分发(QuestionCard) | 按 `question.type` switch 到单选/多选/判断/解答视图;组件**纯展示不碰 provider**,ExamPage 唯一桥接 Riverpod |
| 续答持久化(Riverpod + Isar) | 答案主状态在内存,每次作答异步写 AnswerRecord(Isar);退出重进自动恢复,交卷后按 paperId 清理 |
---
## 四、关键代码剖析
```dart
static const List<int> _sizes = [50, 100, 200];
// 入口页数据流:
// SegmentedButton 选择 → paperSizeProvider(全局)
// → ExamPage 同读该 provider 生成 N 道题 → PageView.builder 懒加载渲染
FilledButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ExamPage()),
);
},
icon: const Icon(Icons.play_arrow),
label: Text('开始练习($size 题)'), // 直接展示当前全局题量
)
```
`_ArchPoint` 是纯展示卡片(图标 + 标题 + 描述),无状态无逻辑——学习型页面的"知识卡片"惯例。
---
## 五、踩坑记录
1. **ConsumerWidget 才能用 ref**:普通 StatelessWidget 里没有 `ref` 参数(要自己 `Consumer` 包一层);
2. `ref.read` 只用在**回调**里,build 里用 `ref.watch`——反了会导致不响应或多余重建;
3. 本页是入口,**真正的考试逻辑在 ExamPage**(`lib/features/exam/exam_page.dart`),别在本页找答案判分逻辑;
4. Web 调试时 Isar 为空实现 → 续答自动降级为纯内存(不落盘),这是设计好的,不是 bug;
5. 想快速体验"恢复进度",答几题后**直接退出页面**(系统返回),而不是杀进程——内存态还在。
---
## 六、扩展思考
- **完整架构必读**:[exam_architecture.md](../exam_architecture.md)(零、新手前置 → 一到九章,含 Riverpod/Isar 教程);
- 题干是 HTML 富文本,渲染方式见 [rich_text_demo.md](rich_text_demo.md);
- 题目/答案的本地持久化模式(Store + watch)见 [isar_watch_demo.md](isar_watch_demo.md);
- 答题记录的"接口 + 本地"双通道思路见 [wrong_flow_demo.md](wrong_flow_demo.md)。