【maaath】Flutter for OpenHarmony 学习答题应用实战开发
随着 OpenHarmony 生态的蓬勃发展,Flutter 作为跨平台开发框架也在积极适配鸿蒙平台。本文将通过一个完整的学习答题应用实例,带领大家掌握 Flutter for OpenHarmony 的开发流程,体验跨平台开发的魅力。作者:maaath学习答题应用是教育类应用中的经典场景,涵盖了题库管理、答题交互、错题回顾、考试测评等核心功能。通过这个项目,我们可以深入学习 Flutter 的状
Flutter for OpenHarmony 学习答题应用实战开发
开源鸿蒙跨平台社区
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
前言
随着 OpenHarmony 生态的蓬勃发展,Flutter 作为跨平台开发框架也在积极适配鸿蒙平台。本文将通过一个完整的学习答题应用实例,带领大家掌握 Flutter for OpenHarmony 的开发流程,体验跨平台开发的魅力。
作者:maaath
一、项目概述
1.1 项目背景
学习答题应用是教育类应用中的经典场景,涵盖了题库管理、答题交互、错题回顾、考试测评等核心功能。通过这个项目,我们可以深入学习 Flutter 的状态管理、网络请求、页面导航等核心能力。
1.2 技术选型
- 跨平台框架:Flutter 3.x (适配 OpenHarmony)
- 状态管理:Provider
- 网络请求:dio
- 本地存储:shared_preferences
- 目标平台:Android / iOS / OpenHarmony
1.3 功能架构
学习答题应用
├── 题库分类列表
├── 答题练习模块
│ ├── 单选题
│ ├── 多选题
│ └── 判断题
├── 错题本管理
├── 模拟考试系统
└ └── 个人中心
二、项目结构
首先,我们来看一下项目的目录结构:
lib/
├── main.dart # 应用入口
├── models/ # 数据模型
│ ├── question_model.dart
│ ├── category_model.dart
│ └── exam_model.dart
├── services/ # 网络服务
│ ├── api_service.dart
│ └── storage_service.dart
├── providers/ # 状态管理
│ ├── quiz_provider.dart
│ └── user_provider.dart
├── pages/ # 页面
│ ├── home_page.dart
│ ├── quiz_page.dart
│ ├── wrong_book_page.dart
│ ├── exam_page.dart
│ └── profile_page.dart
├── widgets/ # 通用组件
│ ├── question_card.dart
│ ├── option_item.dart
│ └── result_dialog.dart
└── utils/ # 工具类
├── constants.dart
└── theme.dart
三、核心数据模型
3.1 题目模型
题目是答题应用的核心数据结构,我们需要支持多种题型:
// lib/models/question_model.dart
/// 题目类型枚举
enum QuestionType {
single, // 单选题
multiple, // 多选题
trueFalse, // 判断题
fill, // 填空题
}
/// 难度等级
enum Difficulty {
easy, // 简单
medium, // 中等
hard, // 困难
}
/// 题目选项
class QuestionOption {
final String id;
final String label;
final String content;
QuestionOption({
required this.id,
required this.label,
required this.content,
});
factory QuestionOption.fromJson(Map<String, dynamic> json) {
return QuestionOption(
id: json['id'] as String,
label: json['label'] as String,
content: json['content'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'label': label,
'content': content,
};
}
}
/// 题目模型
class Question {
final int id;
final int categoryId;
final QuestionType type;
final Difficulty difficulty;
final String question;
final List<QuestionOption>? options;
final dynamic correctAnswer; // string 或 List<String>
final String explanation;
Question({
required this.id,
required this.categoryId,
required this.type,
required this.difficulty,
required this.question,
this.options,
required this.correctAnswer,
required this.explanation,
});
factory Question.fromJson(Map<String, dynamic> json) {
return Question(
id: json['id'] as int,
categoryId: json['categoryId'] as int,
type: QuestionType.values[json['type'] as int],
difficulty: Difficulty.values[json['difficulty'] as int],
question: json['question'] as String,
options: json['options'] != null
? (json['options'] as List)
.map((e) => QuestionOption.fromJson(e as Map<String, dynamic>))
.toList()
: null,
correctAnswer: json['correctAnswer'],
explanation: json['explanation'] as String,
);
}
bool checkAnswer(dynamic userAnswer) {
if (correctAnswer is String && userAnswer is String) {
return correctAnswer == userAnswer;
}
if (correctAnswer is List && userAnswer is List) {
if (correctAnswer.length != userAnswer.length) return false;
return correctAnswer.every((e) => userAnswer.contains(e));
}
return false;
}
}
3.2 题库分类模型
// lib/models/category_model.dart
/// 题库分类
class Category {
final int id;
final String name;
final String icon;
final String color;
final String description;
final int questionCount;
final int completedCount;
double get progressRate =>
questionCount > 0 ? completedCount / questionCount : 0;
Category({
required this.id,
required this.name,
required this.icon,
required this.color,
required this.description,
required this.questionCount,
required this.completedCount,
});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'] as int,
name: json['name'] as String,
icon: json['icon'] as String,
color: json['color'] as String,
description: json['description'] as String,
questionCount: json['questionCount'] as int,
completedCount: json['completedCount'] as int,
);
}
}
四、网络服务层
4.1 API 服务封装
使用 dio 进行网络请求封装,支持统一错误处理和拦截器:
// lib/services/api_service.dart
import 'package:dio/dio.dart';
/// API 服务封装
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
late final Dio _dio;
ApiService._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
},
));
// 添加拦截器
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 添加 token
// options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onResponse: (response, handler) {
handler.next(response);
},
onError: (error, handler) {
// 统一错误处理
handler.next(error);
},
));
}
/// GET 请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
}) async {
return _dio.get<T>(path, queryParameters: queryParameters);
}
/// POST 请求
Future<Response<T>> post<T>(
String path, {
dynamic data,
}) async {
return _dio.post<T>(path, data: data);
}
}
4.2 题库数据服务
// lib/services/quiz_service.dart
import '../models/question_model.dart';
import '../models/category_model.dart';
import 'api_service.dart';
/// 题库数据服务
class QuizService {
final ApiService _api = ApiService();
/// 获取题库分类
Future<List<Category>> getCategories() async {
// 模拟数据,实际项目中替换为真实 API 调用
await Future.delayed(const Duration(milliseconds: 500));
return [
Category(
id: 1,
name: '语文',
icon: '📖',
color: '#FF6B6B',
description: '文学常识、古诗词、文言文',
questionCount: 256,
completedCount: 120,
),
Category(
id: 2,
name: '数学',
icon: '🔢',
color: '#4ECDC4',
description: '代数、几何、概率统计',
questionCount: 320,
completedCount: 85,
),
Category(
id: 3,
name: '英语',
icon: '🔤',
color: '#45B7D1',
description: '词汇、语法、阅读理解',
questionCount: 400,
completedCount: 200,
),
Category(
id: 4,
name: '物理',
icon: '⚡',
color: '#96CEB4',
description: '力学、电学、光学、热学',
questionCount: 280,
completedCount: 65,
),
Category(
id: 5,
name: '化学',
icon: '🧪',
color: '#DDA0DD',
description: '元素周期表、化学反应',
questionCount: 240,
completedCount: 45,
),
Category(
id: 6,
name: '历史',
icon: '📜',
color: '#DEB887',
description: '中国古代史、世界史',
questionCount: 350,
completedCount: 180,
),
];
}
/// 获取题目列表
Future<List<Question>> getQuestions(int categoryId, {int page = 0, int pageSize = 10}) async {
await Future.delayed(const Duration(milliseconds: 500));
// 生成模拟题目数据
return _generateMockQuestions(categoryId, page * pageSize, pageSize);
}
List<Question> _generateMockQuestions(int categoryId, int start, int count) {
final List<Question> questions = [];
for (int i = 0; i < count; i++) {
questions.add(Question(
id: categoryId * 1000 + start + i,
categoryId: categoryId,
type: QuestionType.values[i % 3],
difficulty: Difficulty.values[i % 3],
question: '这是第 ${start + i + 1} 题,${_getCategoryName(categoryId)} 相关题目。请问以下哪个选项是正确的?',
options: [
QuestionOption(id: 'A', label: 'A', content: '选项 A 的内容'),
QuestionOption(id: 'B', label: 'B', content: '选项 B 的内容'),
QuestionOption(id: 'C', label: 'C', content: '选项 C 的内容'),
QuestionOption(id: 'D', label: 'D', content: '选项 D 的内容'),
],
correctAnswer: ['A', 'B'][i % 2],
explanation: '本题考察的是 ${_getCategoryName(categoryId)} 的基础知识,正确答案是 B。',
));
}
return questions;
}
String _getCategoryName(int categoryId) {
const names = ['', '语文', '数学', '英语', '物理', '化学', '历史'];
return names[categoryId] ?? '综合';
}
}
五、状态管理
5.1 答题状态管理
使用 Provider 进行状态管理,实现答题逻辑:
// lib/providers/quiz_provider.dart
import 'package:flutter/foundation.dart';
import '../models/question_model.dart';
import '../models/category_model.dart';
import '../services/quiz_service.dart';
/// 答题状态
enum QuizStatus {
loading,
ready,
answering,
submitted,
completed,
}
/// 答题提供者
class QuizProvider extends ChangeNotifier {
final QuizService _quizService = QuizService();
// 分类列表
List<Category> _categories = [];
List<Category> get categories => _categories;
// 当前题目
List<Question> _questions = [];
List<Question> get questions => _questions;
int _currentIndex = 0;
int get currentIndex => _currentIndex;
Question? get currentQuestion =>
_questions.isNotEmpty && _currentIndex < _questions.length
? _questions[_currentIndex]
: null;
// 答题状态
QuizStatus _status = QuizStatus.loading;
QuizStatus get status => _status;
// 用户答案
final Map<int, dynamic> _userAnswers = {};
dynamic getUserAnswer(int questionId) => _userAnswers[questionId];
// 答题结果
final List<bool> _results = [];
List<bool> get results => _results;
int get correctCount => _results.where((r) => r).length;
double get correctRate =>
_results.isNotEmpty ? correctCount / _results.length : 0;
// 加载分类
Future<void> loadCategories() async {
_status = QuizStatus.loading;
notifyListeners();
try {
_categories = await _quizService.getCategories();
_status = QuizStatus.ready;
} catch (e) {
_status = QuizStatus.ready;
}
notifyListeners();
}
// 加载题目
Future<void> loadQuestions(int categoryId) async {
_status = QuizStatus.loading;
_currentIndex = 0;
_userAnswers.clear();
_results.clear();
notifyListeners();
try {
_questions = await _quizService.getQuestions(categoryId);
_status = QuizStatus.ready;
} catch (e) {
_status = QuizStatus.ready;
}
notifyListeners();
}
// 选择答案
void selectAnswer(String optionId) {
if (_status != QuizStatus.ready) return;
final question = currentQuestion;
if (question == null) return;
if (question.type == QuestionType.multiple) {
// 多选题:切换选择状态
final current = _userAnswers[question.id];
if (current == null) {
_userAnswers[question.id] = [optionId];
} else if (current is List && current.contains(optionId)) {
_userAnswers[question.id] = current.where((e) => e != optionId).toList();
} else {
_userAnswers[question.id] = [...current, optionId];
}
} else {
// 单选题/判断题:直接设置
_userAnswers[question.id] = optionId;
}
_status = QuizStatus.answering;
notifyListeners();
}
// 提交答案
void submitAnswer() {
if (_status != QuizStatus.answering) return;
final question = currentQuestion;
if (question == null) return;
final userAnswer = _userAnswers[question.id];
final isCorrect = question.checkAnswer(userAnswer);
_results.add(isCorrect);
_status = QuizStatus.submitted;
notifyListeners();
}
// 下一题
void nextQuestion() {
if (_currentIndex < _questions.length - 1) {
_currentIndex++;
_status = QuizStatus.ready;
notifyListeners();
} else {
_status = QuizStatus.completed;
notifyListeners();
}
}
// 重置答题
void reset() {
_currentIndex = 0;
_userAnswers.clear();
_results.clear();
_status = QuizStatus.ready;
notifyListeners();
}
}
六、页面实现
6.1 首页 - 题库分类
// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/quiz_provider.dart';
import '../models/category_model.dart';
import 'quiz_page.dart';
/// 首页 - 题库分类列表
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<QuizProvider>().loadCategories();
});
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
body: SafeArea(
child: Column(
children: [
_buildHeader(),
Expanded(child: _buildCategoryList()),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Text(
'📚 题库分类',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
],
),
);
}
Widget _buildCategoryList() {
return Consumer<QuizProvider>(
builder: (context, provider, _) {
if (provider.status == QuizStatus.loading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return RefreshIndicator(
onRefresh: () => provider.loadCategories(),
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: provider.categories.length,
itemBuilder: (context, index) {
return _CategoryCard(
category: provider.categories[index],
onTap: () => _navigateToQuiz(provider.categories[index]),
);
},
),
);
},
);
}
void _navigateToQuiz(Category category) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => QuizPage(category: category),
),
);
}
}
/// 分类卡片组件
class _CategoryCard extends StatelessWidget {
final Category category;
final VoidCallback onTap;
const _CategoryCard({
required this.category,
required this.onTap,
});
Widget build(BuildContext context) {
final color = _parseColor(category.color);
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 图标
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
category.icon,
style: const TextStyle(fontSize: 28),
),
),
),
const SizedBox(width: 12),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 4),
Text(
category.description,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// 进度条
Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: category.progressRate,
backgroundColor: const Color(0xFFE5E7EB),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
),
),
const SizedBox(width: 8),
Text(
'${category.completedCount}/${category.questionCount}',
style: const TextStyle(
fontSize: 10,
color: Color(0xFF9CA3AF),
),
),
],
),
],
),
),
const Icon(
Icons.chevron_right,
color: Color(0xFF9CA3AF),
),
],
),
),
);
}
Color _parseColor(String colorStr) {
try {
final hex = colorStr.replaceFirst('#', '');
return Color(int.parse('FF$hex', radix: 16));
} catch (e) {
return const Color(0xFF6366F1);
}
}
}
6.2 答题页面
// lib/pages/quiz_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/quiz_provider.dart';
import '../models/question_model.dart';
import '../models/category_model.dart';
/// 答题页面
class QuizPage extends StatefulWidget {
final Category category;
const QuizPage({super.key, required this.category});
State<QuizPage> createState() => _QuizPageState();
}
class _QuizPageState extends State<QuizPage> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<QuizProvider>().loadQuestions(widget.category.id);
});
}
void dispose() {
_animationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final color = _parseColor(widget.category.color);
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1F2937)),
onPressed: () => Navigator.pop(context),
),
title: Consumer<QuizProvider>(
builder: (context, provider, _) {
return Column(
children: [
Text(
widget.category.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF1F2937),
),
),
if (provider.questions.isNotEmpty)
Text(
'${provider.currentIndex + 1}/${provider.questions.length}',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
);
},
),
centerTitle: true,
),
body: Consumer<QuizProvider>(
builder: (context, provider, _) {
if (provider.status == QuizStatus.loading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: color),
const SizedBox(height: 16),
const Text(
'加载题目中...',
style: TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
),
),
],
),
);
}
if (provider.questions.isEmpty) {
return const Center(
child: Text(
'暂无题目',
style: TextStyle(
fontSize: 16,
color: Color(0xFF6B7280),
),
),
);
}
final question = provider.currentQuestion;
if (question == null) return const SizedBox();
return Column(
children: [
_buildProgress(provider),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_QuestionCard(
question: question,
provider: provider,
color: color,
onSelect: _handleSelect,
),
if (provider.status == QuizStatus.submitted) ...[
const SizedBox(height: 16),
_ResultCard(question: question, provider: provider),
const SizedBox(height: 16),
_buildNextButton(provider, color),
],
],
),
),
),
],
);
},
),
);
}
Widget _buildProgress(QuizProvider provider) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.white,
child: Row(
children: List.generate(
provider.questions.length.clamp(0, 20),
(index) => Expanded(
child: Container(
height: 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: index < provider.currentIndex
? const Color(0xFF6366F1)
: index == provider.currentIndex
? const Color(0xFF6366F1).withOpacity(0.5)
: const Color(0xFFE5E7EB),
borderRadius: BorderRadius.circular(2),
),
),
),
),
),
);
}
void _handleSelect(String optionId) {
final provider = context.read<QuizProvider>();
provider.selectAnswer(optionId);
}
Widget _buildNextButton(QuizProvider provider, Color color) {
final isLast = provider.currentIndex >= provider.questions.length - 1;
return Column(
children: [
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
if (provider.status == QuizStatus.answering) {
provider.submitAnswer();
} else {
provider.nextQuestion();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Text(
provider.status == QuizStatus.answering
? '确认答案'
: isLast
? '完成答题'
: '下一题',
style: const TextStyle(fontSize: 16),
),
),
),
if (isLast && provider.status == QuizStatus.submitted)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'已经是最后一题了',
style: TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
),
],
);
}
Color _parseColor(String colorStr) {
try {
final hex = colorStr.replaceFirst('#', '');
return Color(int.parse('FF$hex', radix: 16));
} catch (e) {
return const Color(0xFF6366F1);
}
}
}
/// 题目卡片
class _QuestionCard extends StatelessWidget {
final Question question;
final QuizProvider provider;
final Color color;
final Function(String) onSelect;
const _QuestionCard({
required this.question,
required this.provider,
required this.color,
required this.onSelect,
});
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标签
Row(
children: [
_buildTag(_getTypeName(question.type), color),
const SizedBox(width: 8),
_buildTag(_getDifficultyName(question.difficulty), _getDifficultyColor(question.difficulty)),
const Spacer(),
const Text(
'收藏',
style: TextStyle(
fontSize: 12,
color: Color(0xFF9CA3AF),
),
),
],
),
const SizedBox(height: 16),
// 题目
Text(
question.question,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF1F2937),
height: 1.5,
),
),
const SizedBox(height: 24),
// 选项
if (question.options != null)
...question.options!.map((option) => _OptionItem(
option: option,
question: question,
provider: provider,
color: color,
onTap: onSelect,
)),
],
),
);
}
Widget _buildTag(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: TextStyle(
fontSize: 12,
color: color,
),
),
);
}
String _getTypeName(QuestionType type) {
switch (type) {
case QuestionType.single:
return '单选题';
case QuestionType.multiple:
return '多选题';
case QuestionType.trueFalse:
return '判断题';
case QuestionType.fill:
return '填空题';
}
}
String _getDifficultyName(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy:
return '简单';
case Difficulty.medium:
return '中等';
case Difficulty.hard:
return '困难';
}
}
Color _getDifficultyColor(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy:
return const Color(0xFF10B981);
case Difficulty.medium:
return const Color(0xFFF59E0B);
case Difficulty.hard:
return const Color(0xFFEF4444);
}
}
}
/// 选项组件
class _OptionItem extends StatelessWidget {
final QuestionOption option;
final Question question;
final QuizProvider provider;
final Color color;
final Function(String) onTap;
const _OptionItem({
required this.option,
required this.question,
required this.provider,
required this.color,
required this.onTap,
});
Widget build(BuildContext context) {
final userAnswer = provider.getUserAnswer(question.id);
final isSelected = userAnswer is String && userAnswer == option.id ||
userAnswer is List && userAnswer.contains(option.id);
final isSubmitted = provider.status == QuizStatus.submitted;
final isCorrect = question.correctAnswer is String && question.correctAnswer == option.id ||
question.correctAnswer is List && (question.correctAnswer as List).contains(option.id);
Color bgColor;
Color textColor;
if (isSubmitted) {
if (isCorrect) {
bgColor = const Color(0xFF10B981);
textColor = Colors.white;
} else if (isSelected && !isCorrect) {
bgColor = const Color(0xFFEF4444);
textColor = Colors.white;
} else {
bgColor = const Color(0xFFE5E7EB);
textColor = const Color(0xFF6B7280);
}
} else {
bgColor = isSelected ? color : const Color(0xFFF3F4F6);
textColor = isSelected ? Colors.white : const Color(0xFF1F2937);
}
return GestureDetector(
onTap: isSubmitted ? null : () => onTap(option.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: isSelected && !isSubmitted
? Border.all(color: color, width: 2)
: null,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected ? Colors.white.withOpacity(0.2) : const Color(0xFFE5E7EB),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
option.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: textColor,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
option.content,
style: TextStyle(
fontSize: 14,
color: textColor,
),
),
),
if (isSubmitted && isCorrect)
const Icon(Icons.check_circle, color: Colors.white, size: 20),
if (isSubmitted && isSelected && !isCorrect)
const Icon(Icons.cancel, color: Colors.white, size: 20),
],
),
),
);
}
}
/// 结果卡片
class _ResultCard extends StatelessWidget {
final Question question;
final QuizProvider provider;
const _ResultCard({
required this.question,
required this.provider,
});
Widget build(BuildContext context) {
final isCorrect = provider.results.isNotEmpty && provider.results.last;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isCorrect
? const Color(0xFF10B981).withOpacity(0.1)
: const Color(0xFFEF4444).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
children: [
Text(
isCorrect ? '🎉' : '😢',
style: const TextStyle(fontSize: 32),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isCorrect ? '回答正确' : '回答错误',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isCorrect
? const Color(0xFF10B981)
: const Color(0xFFEF4444),
),
),
Text(
isCorrect ? '太棒了,继续加油!' : '别灰心,再接再厉!',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6B7280),
),
),
],
),
],
),
if (!isCorrect) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Text(
'正确答案:',
style: TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
),
),
Text(
question.correctAnswer is List
? (question.correctAnswer as List).join('、')
: question.correctAnswer.toString(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF10B981),
),
),
],
),
),
],
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📖 答案解析',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 8),
Text(
question.explanation,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF6B7280),
height: 1.5,
),
),
],
),
),
],
),
);
}
}
七、运行截图
以下是应用在鸿蒙设备上的运行效果:
7.1 题库分类列表

7.2 错题界面

7.3 模拟考试

八、代码仓库
本文涉及的完整代码已托管至 AtomGit 平台:
仓库地址:https://atomgit.com/maaath/quiz_app_flutter_ohos
仓库包含以下内容:
lib/- 应用核心代码assets/- 应用资源文件README.md- 项目说明文档pubspec.yaml- 项目依赖配置
九、总结与展望
通过本文,我们完整实现了一个 Flutter for OpenHarmony 的学习答题应用。项目涵盖了以下核心知识点:
- 跨平台架构设计:使用 Flutter 实现一次开发、多平台运行
- 状态管理方案:采用 Provider 进行应用状态管理
- 网络请求封装:使用 dio 实现统一的网络请求处理
- UI 组件化开发:将常用组件抽取为独立 Widget
- 动画效果实现:使用 AnimatedContainer 实现选项点击动效
后续扩展方向
- 添加真实 API 接口对接
- 实现错题本持久化存储
- 开发更多的题型支持
- 添加学习数据分析功能
- 支持多语言国际化
参考资料
- Flutter 官方文档:https://flutter.dev/docs
- OpenHarmony 开发者文档:https://developer.harmonyos.com
- Provider 状态管理:https://pub.dev/packages/provider
- Dio 网络库:https://pub.dev/packages/dio
更多推荐




所有评论(0)