# 【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( 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 _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)。