Flutter三方库适配OpenHarmony【quiz_app】Flutter知识测验应用项目完整实战

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

quiz_app 是一个典型的 Flutter 知识测验应用。它通过 Question 模型组织题目、选项和正确答案索引,使用 _currentQuestionIndex 控制当前题目,使用 _score 记录答对数量,使用 _selectedIndex_answered 锁定单题答题状态,并在最后一题完成后弹出结果对话框。

测验类应用非常适合练习 Flutter 的 数据模型、列表题库、单选交互、答题锁定、颜色反馈、进度条、结果弹窗、ListView 选项渲染和 OpenHarmony 触摸适配。它的业务逻辑清晰,页面结构也完整,是很好的入门实战案例。

在这里插入图片描述

图示说明:本文围绕 Flutter 工程中的 quiz_app 项目展开,重点分析题库模型、答题流程、得分统计、选项反馈、结果弹窗和 OpenHarmony 适配关注点。

测验应用的核心是“题目展示、单次选择、即时反馈、进入下一题、最终统计”这一条稳定的答题链路。

本文将基于项目真实源码展开,核心内容包括:

  • QuizApp 的应用入口与靛蓝色 Material 3 主题
  • Question 模型如何表达题干、选项和正确答案
  • _questions 如何组织 10 道 Flutter 基础题
  • _selectAnswer() 如何锁定答题并更新分数
  • _nextQuestion() 如何切换下一题或显示结果
  • _showResultsDialog() 如何展示最终成绩
  • ListView 如何渲染 4 个选项并高亮正确答案
  • 当前题目固定顺序、没有错题回顾的真实边界
  • OpenHarmony 适配时需要验证的进度条、列表、触摸和弹窗

一、项目背景与目标

1.1 应用定位

quiz_app 的定位是一个 Flutter 基础知识测验应用。它内置 10 道题,每题有 4 个选项,用户选择一个答案后立即看到正确和错误反馈,然后点击按钮进入下一题。最后一题完成后,应用弹出成绩对话框。

从用户视角看,流程是:

  1. 打开应用,看到第 1 道题。
  2. 点击一个选项。
  3. 页面高亮正确答案和错误选择。
  4. 点击 Next Question
  5. 完成最后一题后点击 See Results
  6. 查看最终分数。
  7. 点击 Play Again 重新开始。

从工程视角看,流程是:

  1. 使用 Question 模型定义题目。
  2. 使用 _currentQuestionIndex 定位当前题。
  3. 使用 _selectedIndex 记录用户选择。
  4. 使用 _answered 锁定当前题。
  5. 答对时 _score 加 1。
  6. 切题时重置选择状态。
  7. 结束时展示结果弹窗。

1.2 当前功能概览

功能 当前实现 技术点
应用入口 runApp(const QuizApp()) Flutter 启动流程
应用主题 靛蓝色 Material 3 ColorScheme.fromSeed
题目模型 Question 题干、选项、正确索引
内置题库 10 道 Flutter 基础题 List<Question>
当前题号 _currentQuestionIndex 题目切换
得分 _score 答对题数
用户选择 _selectedIndex 单题选中项
答题锁定 _answered 防止重复选择
顶部进度 LinearProgressIndicator 当前答题进度
结果弹窗 AlertDialog 最终分数和评价

1.3 适合学习的能力

这个项目适合学习:

  • 测验题库模型设计
  • 单选题交互流程
  • 答题锁定状态
  • ListView 选项渲染
  • 正确和错误颜色反馈
  • 进度条与题号同步
  • 结果弹窗和重开逻辑
  • OpenHarmony 下列表与触摸验证

二、环境准备与工程结构

2.1 技术栈概览

项目只使用 Flutter SDK 自带能力,没有接入网络或存储。

类别 当前使用 说明
开发语言 Dart Flutter 应用主语言
UI 框架 Flutter Material 页面、卡片、列表、弹窗
状态管理 StatefulWidget + setState 答题状态刷新
数据模型 Question 题目结构
进度组件 LinearProgressIndicator 题目进度和成绩进度
列表组件 ListView.builder 渲染选项
反馈组件 AlertDialog 最终成绩
目标适配 Flutter / OpenHarmony UI、列表、触摸验证

2.2 pubspec 关键配置

工程配置如下:

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

当前项目没有远程题库、数据库或本地持久化能力,题目全部写在 Dart 源码里。

2.3 主源码结构

核心代码集中在 lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const QuizApp());
}

主要结构如下:

结构 类型 作用
QuizApp StatelessWidget 应用根组件
Question 普通 Dart 类 题目模型
QuizHomePage StatefulWidget 测验首页
_QuizHomePageState State 管理题目、得分和答题状态

2.4 常用运行命令

完成 Flutter 环境准备后,可以执行:

flutter pub get
flutter analyze
flutter test
flutter run

OpenHarmony 环境运行时,需要结合本地 Flutter OpenHarmony 发行版、DevEco Studio、设备连接和签名配置。

三、应用入口与主题配置

3.1 main 函数

应用入口如下:

void main() {
  runApp(const QuizApp());
}

runApp 会把根组件挂载到 Flutter 渲染树。

3.2 QuizApp 根组件

根组件代码如下:

class QuizApp extends StatelessWidget {
  const QuizApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Quiz App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const QuizHomePage(title: 'Quiz App'),
    );
  }
}

它完成了:

  1. 设置应用标题为 Quiz App
  2. 使用靛蓝色作为 Material 3 种子色。
  3. 将首页设置为 QuizHomePage

3.3 靛蓝色主题的意义

靛蓝色适合知识类、学习类和工具类应用。项目中它主要用于:

  • AppBar 主题背景。
  • 未答题状态下选项头像背景。
  • Next Question 按钮背景。
  • 页面整体学习氛围。

四、Question 模型设计

4.1 模型定义

题目模型如下:

class Question {
  String question;
  List<String> options;
  int correctIndex;

  Question({
    required this.question,
    required this.options,
    required this.correctIndex,
  });
}

字段含义如下:

字段 类型 作用
question String 题干
options List<String> 选项列表
correctIndex int 正确答案索引

4.2 为什么用 correctIndex

使用索引可以让正确答案和选项列表保持绑定:

correctIndex: 1

如果选项是:

['Java', 'Dart', 'Kotlin', 'Swift']

索引 1 就代表 Dart

4.3 模型可变字段边界

当前 Question 的字段不是 final,理论上可以被修改。但源码中题库创建后没有修改题目内容。

如果希望模型更稳,可以写成:

class Question {
  final String question;
  final List<String> options;
  final int correctIndex;

  const Question({
    required this.question,
    required this.options,
    required this.correctIndex,
  });
}

这样题目数据更接近不可变配置。

五、题库结构

5.1 _questions 列表

项目内置 10 道题:

final List<Question> _questions = [
  Question(
    question: 'What programming language is used by Flutter?',
    options: ['Java', 'Dart', 'Kotlin', 'Swift'],
    correctIndex: 1,
  ),
  Question(
    question: 'What company developed Flutter?',
    options: ['Facebook', 'Google', 'Microsoft', 'Apple'],
    correctIndex: 1,
  ),
];

题库主题集中在 Flutter 基础知识。

5.2 当前题目范围

主题 涉及问题
Flutter 语言 Dart
Flutter 来源 Google
Widget UI building block
Hot Reload 快速刷新代码变化
pubspec.yaml 依赖配置
build 方法 描述 UI
StatefulWidget 可变状态内容
setState 触发重建
MaterialApp Material 应用入口
BuildContext 访问主题和导航

5.3 固定顺序边界

当前题目按列表顺序展示,没有随机排序,也没有打乱选项。每次重新开始都会从第 1 题开始。

如果做成正式测验,可以增加题目随机和选项随机。

5.4 本地题库边界

题库写在源码中,意味着:

  • 不需要网络。
  • 启动即可答题。
  • 修改题目需要改代码。
  • 没有题库分类。
  • 没有错题本。
  • 没有远程题库更新。

六、答题状态设计

6.1 状态字段

答题状态如下:

int _currentQuestionIndex = 0;
int _score = 0;
int? _selectedIndex;
bool _answered = false;

字段含义:

字段 作用
_currentQuestionIndex 当前题目索引
_score 当前得分
_selectedIndex 当前题选择的选项索引
_answered 当前题是否已经作答

6.2 当前题目读取

build 方法中读取当前题:

final question = _questions[_currentQuestionIndex];

页面中所有题干、选项、正确答案判断都基于这个对象。

6.3 答题锁定

选择答案时首先判断:

if (_answered) return;

这能防止用户在同一题中反复点击多个选项刷分。

6.4 状态重置

进入下一题时会重置:

_selectedIndex = null;
_answered = false;

这样下一题会重新进入可选择状态。

七、答案选择与得分

7.1 _selectAnswer 方法

选择答案逻辑如下:

void _selectAnswer(int index) {
  if (_answered) return;

  setState(() {
    _selectedIndex = index;
    _answered = true;
  });

  if (index == _questions[_currentQuestionIndex].correctIndex) {
    setState(() {
      _score++;
    });
  }
}

7.2 选择后发生什么

用户点击某个选项后:

  1. _selectedIndex 记录选择。
  2. _answered 变为 true。
  3. 如果选择正确,_score 加 1。
  4. 页面显示正确和错误反馈。
  5. 底部显示 Next Question 或 See Results。

7.3 正确答案判断

判断代码如下:

index == _questions[_currentQuestionIndex].correctIndex

因为每题只允许选择一次,所以得分最多增加一次。

7.4 两次 setState 的边界

当前答题正确时会调用两次 setState。它能正常工作,但可以合并为一次:

setState(() {
  _selectedIndex = index;
  _answered = true;
  if (index == _questions[_currentQuestionIndex].correctIndex) {
    _score++;
  }
});

合并后状态更新更集中。

八、选项列表与颜色反馈

8.1 ListView.builder 渲染选项

选项使用列表渲染:

ListView.builder(
  itemCount: question.options.length,
  itemBuilder: (context, index) {
    final isCorrect = index == question.correctIndex;
    final isSelected = _selectedIndex == index;
    Color? cardColor;
    return Card(...);
  },
)

每道题有 4 个选项。

8.2 答题后的卡片颜色

答题后设置颜色:

if (_answered) {
  if (isCorrect) {
    cardColor = Colors.green.shade100;
  } else if (isSelected && !isCorrect) {
    cardColor = Colors.red.shade100;
  }
}

颜色含义:

状态 颜色
正确答案 浅绿色
用户选错项 浅红色
未选且非正确项 默认色

8.3 CircleAvatar 选项标识

每个选项左侧显示 A、B、C、D:

String.fromCharCode(65 + index)

65 是字符 A 的编码,因此:

index 显示
0 A
1 B
2 C
3 D

8.4 trailing 图标

答题后显示图标:

trailing: _answered
    ? Icon(
        isCorrect
            ? Icons.check_circle
            : isSelected
                ? Icons.cancel
                : null,
        color: isCorrect ? Colors.green : Colors.red,
      )
    : null

正确答案显示 check,错误选择显示 cancel。

九、进度条与题号展示

9.1 顶部进度条

页面顶部有进度条:

LinearProgressIndicator(
  value: (_currentQuestionIndex + 1) / _questions.length,
  backgroundColor: Colors.grey.shade200,
)

它表示当前所在题目进度。

9.2 题号文本

题号显示:

Text(
  'Question ${_currentQuestionIndex + 1}/${_questions.length}',
  style: const TextStyle(fontWeight: FontWeight.bold),
)

9.3 得分文本

当前分数显示:

Text(
  'Score: $_score',
  style: const TextStyle(fontWeight: FontWeight.bold),
)

9.4 进度含义边界

顶部进度条显示的是当前题号进度,不是完成题数进度。因此第 1 题时进度已经是 1/10,而不是 0/10。

这是合理设计,但需要明确它表达的是“当前题位置”。

十、下一题与结果弹窗

10.1 _nextQuestion 方法

下一题逻辑如下:

void _nextQuestion() {
  if (_currentQuestionIndex < _questions.length - 1) {
    setState(() {
      _currentQuestionIndex++;
      _selectedIndex = null;
      _answered = false;
    });
  } else {
    _showResultsDialog();
  }
}

如果还没到最后一题,就进入下一题;如果已经是最后一题,就展示结果。

10.2 底部按钮显示

只有答题后才显示按钮:

if (_answered)
  Padding(
    child: ElevatedButton(
      onPressed: _nextQuestion,
      child: Text(
        _currentQuestionIndex < _questions.length - 1
            ? 'Next Question'
            : 'See Results',
      ),
    ),
  )

这能保证用户必须选择答案后才能继续。

10.3 结果弹窗

最后一题后显示结果弹窗:

void _showResultsDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(...),
  );
}

弹窗包含成绩、评价和进度条。

10.4 重新开始

弹窗中的 Play Again 会执行:

Navigator.pop(context);
_restartQuiz();

关闭弹窗后重置测验状态。

十一、结果评估逻辑

11.1 分数展示

结果弹窗中显示:

Text(
  'Score: $_score / ${_questions.length}',
  style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
)

11.2 评价阈值

评价条件如下:

_score >= _questions.length ~/ 2
    ? 'Great job!'
    : 'Keep learning!'

当前题目总数是 10,10 ~/ 2 为 5,因此得分大于等于 5 时显示 Great job!

11.3 结果进度条

结果弹窗中还有成绩进度条:

LinearProgressIndicator(
  value: _score / _questions.length,
  backgroundColor: Colors.grey.shade200,
  valueColor: AlwaysStoppedAnimation(
    _score >= _questions.length ~/ 2 ? Colors.green : Colors.orange,
  ),
)

它表示答对比例。

11.4 结果页边界

当前结果只在弹窗中展示,没有错题回顾、答案解析、答题用时或保存历史成绩。

如果要做成完整学习应用,这些都是后续扩展点。

十二、OpenHarmony 适配要点

12.1 适配关注范围

quiz_app 没有平台插件,重点验证 Flutter UI、列表、触摸和弹窗。

适配项 涉及源码 验证重点
MaterialApp 根组件 应用启动和主题
LinearProgressIndicator 顶部进度和结果进度 进度显示
Card 题干和选项 布局、颜色
ListView.builder 选项列表 滚动与渲染
ListTile 单个选项 点击和图标
CircleAvatar A/B/C/D 标识 颜色和文本
AlertDialog 结果弹窗 展示和关闭
ElevatedButton 下一题、重开 点击状态
Material Icons check、cancel、结果图标 字体资源

12.2 图标资源验证

项目使用了:

Icons.emoji_events
Icons.sentiment_dissatisfied
Icons.check_circle
Icons.cancel

OpenHarmony 设备上需要验证:

  1. 图标是否正常显示。
  2. 图标颜色是否符合预期。
  3. 图标和选项文字是否对齐。
  4. Material Icons 字体是否随包体加载。

12.3 列表触摸验证

选项点击依赖:

ListTile(
  onTap: () => _selectAnswer(index),
)

需要验证:

  • 每个选项都能点击。
  • 答题后再次点击不会重复计分。
  • 正确答案是否高亮。
  • 错误选择是否标红。

12.4 小屏幕验证

题干和选项可能较长,例如:

'What is the difference between StatelessWidget and StatefulWidget?'

需要观察:

  • 题干是否自动换行。
  • 选项是否被截断。
  • 底部按钮是否遮挡列表。
  • 结果弹窗是否溢出。

十三、测试与验证

13.1 静态分析

建议执行:

flutter analyze

重点关注:

  • Question 模型字段是否需要不可变。
  • 两次 setState 是否可以合并。
  • 结果弹窗逻辑是否覆盖最后一题。
  • 选项数量和 correctIndex 是否匹配。

13.2 组件测试方向

可以执行:

flutter test

适合覆盖的行为包括:

  1. 初始显示第 1 题。
  2. 初始 Score 为 0。
  3. 点击正确选项后 Score 增加。
  4. 答题后 Next Question 按钮出现。
  5. 点击下一题后题号增加。
  6. 最后一题后显示结果弹窗。

13.3 示例测试代码

下面是一段基础页面测试思路:

testWidgets('shows first quiz question', (tester) async {
  await tester.pumpWidget(const QuizApp());

  expect(find.text('Question 1/10'), findsOneWidget);
  expect(find.text('Score: 0'), findsOneWidget);
  expect(find.text('What programming language is used by Flutter?'), findsOneWidget);
});

这能确认初始题目和统计信息正常。

13.4 答题测试思路

第一题正确答案是 Dart:

testWidgets('selecting correct answer increases score', (tester) async {
  await tester.pumpWidget(const QuizApp());

  await tester.tap(find.text('Dart'));
  await tester.pumpAndSettle();

  expect(find.text('Score: 1'), findsOneWidget);
  expect(find.text('Next Question'), findsOneWidget);
});

这能验证答题、计分和下一题按钮。

13.5 手动验证流程

手动验证可以按如下顺序进行:

  1. 启动应用,确认显示第 1 题。
  2. 点击错误选项,确认错误项标红且正确项标绿。
  3. 点击正确选项,确认 Score 增加。
  4. 答题后确认 Next Question 按钮出现。
  5. 完成 10 道题后点击 See Results。
  6. 确认结果弹窗显示分数和评价。
  7. 点击 Play Again,确认回到第 1 题并清零。
  8. 在 OpenHarmony 设备上验证列表触摸和弹窗表现。

十四、常见问题与优化建议

14.1 为什么答题后不能改答案

因为 _answered 会被设置为 true:

if (_answered) return;

这能防止用户反复点击不同选项刷分。

14.2 为什么题目顺序每次都一样

因为题库按 _questions 列表顺序展示,没有 shuffle。重新开始后仍从第 1 题开始。

如果要增加挑战,可以在开始测验时打乱题目顺序。

14.3 为什么没有错题解析

当前源码只展示颜色反馈和最终分数,没有 explanation 字段。要增加解析,需要扩展 Question 模型。

14.4 为什么 5 分就显示 Great job

当前阈值是:

_score >= _questions.length ~/ 2

10 道题的一半是 5,因此 5 分及以上显示 Great job!

14.5 是否需要持久化成绩

当前分数只存在内存中,应用重启后清零。正式学习应用通常会保存历史成绩和错题记录。

十五、工程扩展方向

15.1 增加题目解析

可以扩展模型:

class Question {
  final String question;
  final List<String> options;
  final int correctIndex;
  final String explanation;

  const Question({
    required this.question,
    required this.options,
    required this.correctIndex,
    required this.explanation,
  });
}

答题后展示解析,学习价值会更高。

15.2 打乱题目顺序

可以在重新开始时复制并打乱题库:

final shuffled = List<Question>.from(_questions)..shuffle();

如果要保持原题库不变,可以单独维护当前答题列表。

15.3 增加错题本

可以记录错误题目索引:

final List<int> _wrongQuestionIndexes = [];

当选择错误时加入:

_wrongQuestionIndexes.add(_currentQuestionIndex);

结果页可以展示错题数量和错题列表。

15.4 增加计时功能

可以为每题增加倒计时:

int _remainingSeconds = 30;

时间结束后自动判为未答或进入下一题。

15.5 增加多分类题库

可以把题目分成:

  • Flutter 基础
  • Dart 语法
  • Widget 布局
  • 状态管理
  • 工程配置

用户可以选择分类后再答题。

十六、完整核心代码回顾

16.1 应用入口

void main() {
  runApp(const QuizApp());
}

入口负责启动根组件。

16.2 题目模型

class Question {
  String question;
  List<String> options;
  int correctIndex;

  Question({
    required this.question,
    required this.options,
    required this.correctIndex,
  });
}

模型保存题干、选项和正确答案索引。

16.3 选择答案

void _selectAnswer(int index) {
  if (_answered) return;

  setState(() {
    _selectedIndex = index;
    _answered = true;
  });

  if (index == _questions[_currentQuestionIndex].correctIndex) {
    setState(() {
      _score++;
    });
  }
}

这是答题锁定和计分的核心。

16.4 下一题

void _nextQuestion() {
  if (_currentQuestionIndex < _questions.length - 1) {
    setState(() {
      _currentQuestionIndex++;
      _selectedIndex = null;
      _answered = false;
    });
  } else {
    _showResultsDialog();
  }
}

它负责切题或展示结果。

16.5 重开测验

void _restartQuiz() {
  setState(() {
    _currentQuestionIndex = 0;
    _score = 0;
    _selectedIndex = null;
    _answered = false;
  });
}

重开会清空状态并回到第 1 题。

16.6 结果评价

_score >= _questions.length ~/ 2
    ? 'Great job!'
    : 'Keep learning!'

当前逻辑以总题数一半作为评价阈值。

总结

quiz_app 用 Flutter 实现了一个完整的知识测验应用:它通过 Question 模型组织题目,通过 _currentQuestionIndex 控制当前题,通过 _selectedIndex_answered 锁定单题选择,通过 _score 记录答对数量,通过 ListView 渲染选项,并在最后一题完成后用 AlertDialog 展示最终成绩。

从 OpenHarmony 适配角度看,这个项目覆盖了 Material 主题、LinearProgressIndicator、Card、ListView、ListTile、CircleAvatar、Icon、ElevatedButton 和 AlertDialog 等基础能力,很适合验证 Flutter 测验类页面在 OpenHarmony 上的表现。

当前源码也有几个真实边界:题目固定顺序展示,没有随机化;结果页没有错题回顾和答案解析;Question 模型字段可变但当前没有修改;正确答题时使用了两次 setState,可以合并;成绩没有持久化。这些边界不影响项目作为入门实战案例使用,但在继续工程化时可以优先扩展题目解析、错题本和历史成绩。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐