Flutter三方库适配OpenHarmony【quiz_app】Flutter知识测验应用项目完整实战
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 道题。
- 点击一个选项。
- 页面高亮正确答案和错误选择。
- 点击
Next Question。 - 完成最后一题后点击
See Results。 - 查看最终分数。
- 点击
Play Again重新开始。
从工程视角看,流程是:
- 使用
Question模型定义题目。 - 使用
_currentQuestionIndex定位当前题。 - 使用
_selectedIndex记录用户选择。 - 使用
_answered锁定当前题。 - 答对时
_score加 1。 - 切题时重置选择状态。
- 结束时展示结果弹窗。
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'),
);
}
}
它完成了:
- 设置应用标题为
Quiz App。 - 使用靛蓝色作为 Material 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 来源 | |
| 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 选择后发生什么
用户点击某个选项后:
_selectedIndex记录选择。_answered变为 true。- 如果选择正确,
_score加 1。 - 页面显示正确和错误反馈。
- 底部显示 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 设备上需要验证:
- 图标是否正常显示。
- 图标颜色是否符合预期。
- 图标和选项文字是否对齐。
- 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 题。
- 初始 Score 为 0。
- 点击正确选项后 Score 增加。
- 答题后 Next Question 按钮出现。
- 点击下一题后题号增加。
- 最后一题后显示结果弹窗。
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 题。
- 点击错误选项,确认错误项标红且正确项标绿。
- 点击正确选项,确认 Score 增加。
- 答题后确认 Next Question 按钮出现。
- 完成 10 道题后点击 See Results。
- 确认结果弹窗显示分数和评价。
- 点击 Play Again,确认回到第 1 题并清零。
- 在 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,可以合并;成绩没有持久化。这些边界不影响项目作为入门实战案例使用,但在继续工程化时可以优先扩展题目解析、错题本和历史成绩。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 官网:https://www.openharmony.cn
- OpenHarmony 文档:https://docs.openharmony.cn
- Gitee OpenHarmony:https://gitee.com/openharmony
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Flutter 官网:https://flutter.dev
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)