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

4.9 KiB
Raw Blame History

【Demo】试卷练习入口(大数据滑动验证页)

源文件:lib/pages/demo/exam_demo.dart | 分类:考试(exam) 一句话:试卷练习的入口页 —— 选题目数量(50/100/200)→ 进入真实考试页,验证 PageView 大数据滑动不卡。


一、这个 Demo 解决什么问题

真实考试页(lib/features/exam/exam_page.dart)是本项目的核心功能,完整架构见 exam_architecture.md(建议先读)。本页是它的演示入口,回答三个问题:

  1. 100 道题滑动卡不卡? —— PageView.builder 懒加载:只构建视口 ±1 页,滑出的页面自动回收;
  2. 四种题型怎么共存? —— 按 question.type switch 分发到单选/多选/判断/解答卡片;
  3. 答一半退出,进度还在吗? —— Riverpod(内存主状态)+ Isar(AnswerRecord 备份层),重进自动恢复。

对照 Vue:对标 swiper 大数据瓶颈——Vue 会把所有 DOM 挂上,Flutter 的 PageView.builder 只构建看得见的 ±1 页。


二、怎么运行 / 演示内容

runApp(const MaterialApp(home: ExamDemoPage()));
  1. SegmentedButton 选数量(50/100/200 题)→ 点"开始练习";
  2. 快速左右滑动感受流畅度(allowImplicitScrolling 预构建相邻页);
  3. 答几题 → 退出 → 重新进入 → SnackBar 提示"已恢复上次答题进度(X 题),定位到第 Y 题";
  4. 交卷 → 自动判分(客观题)+ 按 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 是纯展示卡片(图标 + 标题 + 描述),无状态无逻辑——学习型页面的"知识卡片"惯例。


五、踩坑记录

  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. 想快速体验"恢复进度",答几题后直接退出页面(系统返回),而不是杀进程——内存态还在。

六、扩展思考