110 lines
4.9 KiB
Markdown
110 lines
4.9 KiB
Markdown
# 【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)。
|