4.9 KiB
4.9 KiB
【Demo】试卷练习入口(大数据滑动验证页)
源文件:
lib/pages/demo/exam_demo.dart| 分类:考试(exam) 一句话:试卷练习的入口页 —— 选题目数量(50/100/200)→ 进入真实考试页,验证 PageView 大数据滑动不卡。
一、这个 Demo 解决什么问题
真实考试页(lib/features/exam/exam_page.dart)是本项目的核心功能,完整架构见
exam_architecture.md(建议先读)。本页是它的演示入口,回答三个问题:
- 100 道题滑动卡不卡? —— PageView.builder 懒加载:只构建视口 ±1 页,滑出的页面自动回收;
- 四种题型怎么共存? —— 按
question.typeswitch 分发到单选/多选/判断/解答卡片; - 答一半退出,进度还在吗? —— Riverpod(内存主状态)+ Isar(AnswerRecord 备份层),重进自动恢复。
对照 Vue:对标 swiper 大数据瓶颈——Vue 会把所有 DOM 挂上,Flutter 的 PageView.builder 只构建看得见的 ±1 页。
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: ExamDemoPage()));
- SegmentedButton 选数量(50/100/200 题)→ 点"开始练习";
- 快速左右滑动感受流畅度(
allowImplicitScrolling预构建相邻页); - 答几题 → 退出 → 重新进入 → SnackBar 提示"已恢复上次答题进度(X 题),定位到第 Y 题";
- 交卷 → 自动判分(客观题)+ 按 paperId 清理本地进度。
三、核心知识点(新手向)
1. 数量选择 = Riverpod 全局状态
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. 进入考试页 = 普通跳转
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ExamPage()),
);
考试页通过同一个 paperSizeProvider 拿题量——传参交给状态管理,路由只负责跳转(对比 router_home_page.md 的构造传参)。
3. 三个架构要点(页面里直接写了卡片)
| 要点 | 说明 |
|---|---|
| PageView.builder 懒加载 | 只构建视口 ±1 页,滑出即回收;allowImplicitScrolling 预构建相邻页 → 100 题滑动不掉帧 |
| 题型分发(QuestionCard) | 按 question.type switch 到单选/多选/判断/解答视图;组件纯展示不碰 provider,ExamPage 唯一桥接 Riverpod |
| 续答持久化(Riverpod + Isar) | 答案主状态在内存,每次作答异步写 AnswerRecord(Isar);退出重进自动恢复,交卷后按 paperId 清理 |
四、关键代码剖析
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 是纯展示卡片(图标 + 标题 + 描述),无状态无逻辑——学习型页面的"知识卡片"惯例。
五、踩坑记录
- ConsumerWidget 才能用 ref:普通 StatelessWidget 里没有
ref参数(要自己Consumer包一层); ref.read只用在回调里,build 里用ref.watch——反了会导致不响应或多余重建;- 本页是入口,真正的考试逻辑在 ExamPage(
lib/features/exam/exam_page.dart),别在本页找答案判分逻辑; - Web 调试时 Isar 为空实现 → 续答自动降级为纯内存(不落盘),这是设计好的,不是 bug;
- 想快速体验"恢复进度",答几题后直接退出页面(系统返回),而不是杀进程——内存态还在。
六、扩展思考
- 完整架构必读:exam_architecture.md(零、新手前置 → 一到九章,含 Riverpod/Isar 教程);
- 题干是 HTML 富文本,渲染方式见 rich_text_demo.md;
- 题目/答案的本地持久化模式(Store + watch)见 isar_watch_demo.md;
- 答题记录的"接口 + 本地"双通道思路见 wrong_flow_demo.md。