作为一名有多年移动开发经验的工程师,我发现Flutter框架在构建跨平台应用方面确实有着独特优势。最近我使用Flutter 3.x完成了一个趣味脑筋急转弯应用的开发,这个项目不仅可以在Android和iOS平台运行,还能适配华为HarmonyOS系统。下面我将详细分享这个项目的完整开发过程和技术要点。
这个应用的核心功能包括:
提示:虽然项目最初是为Android/iOS设计的,但由于Flutter的跨平台特性,只需简单调整就能完美运行在HarmonyOS设备上,这也是我选择Flutter的重要原因之一。
应用采用典型的MVC架构,主要分为以下几个模块:
code复制lib/
├── main.dart # 应用入口
├── models/ # 数据模型
│ └── question.dart # 题目模型
├── pages/ # 页面组件
│ ├── home_page.dart # 首页
│ ├── question_list_page.dart # 题目列表
│ ├── question_detail_page.dart# 答题页
│ ├── challenge_page.dart # 挑战模式
│ └── statistics_page.dart # 统计页面
├── widgets/ # 可复用组件
│ ├── question_card.dart # 题目卡片
│ ├── difficulty_chip.dart # 难度标签
│ └── achievement_card.dart # 成就卡片
└── services/ # 服务层
└── storage_service.dart # 本地存储
Question模型是整个应用的核心数据结构,设计如下:
dart复制class Question {
final int id; // 题目唯一标识
final String question; // 问题文本
final String answer; // 正确答案
final String category; // 分类(动物、物品等)
final String difficulty; // 难度等级
final List<String> hints; // 提示列表(最多3个)
bool isAnswered = false; // 是否已回答
bool isCorrect = false; // 是否答对
// 构造方法
Question({
required this.id,
required this.question,
required this.answer,
required this.category,
required this.difficulty,
required this.hints,
});
// 从JSON创建Question
factory Question.fromJson(Map<String, dynamic> json) {
return Question(
id: json['id'],
question: json['question'],
answer: json['answer'],
category: json['category'],
difficulty: json['difficulty'],
hints: List<String>.from(json['hints']),
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'question': question,
'answer': answer,
'category': category,
'difficulty': difficulty,
'hints': hints,
};
}
}
注意事项:模型中的isAnswered和isCorrect不需要在JSON序列化中处理,因为它们是运行时状态而非持久化数据。
应用支持按难度筛选题目,实现代码如下:
dart复制List<Question> get _filteredQuestions {
// 如果选择"全部"难度,返回所有题目
if (_selectedDifficulty == '全部') {
return widget.questions;
}
// 否则筛选指定难度的题目
return widget.questions
.where((q) => q.difficulty == _selectedDifficulty)
.toList();
}
在UI中使用DropdownButton实现难度选择器:
dart复制DropdownButton<String>(
value: _selectedDifficulty,
items: ['全部', '简单', '中等', '困难'].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedDifficulty = newValue!;
});
},
)
答题页面的核心是答案提交和判断逻辑:
dart复制// 答案输入控制器
final _answerController = TextEditingController();
void _submitAnswer() {
final userAnswer = _answerController.text.trim();
setState(() {
widget.question.isAnswered = true;
// 答案判断(不区分大小写)
widget.question.isCorrect =
userAnswer.toLowerCase() == widget.question.answer.toLowerCase();
});
// 回调通知父组件答题结果
widget.onAnswered(widget.question.isCorrect);
// 显示结果对话框
if (widget.question.isCorrect) {
_showSuccessDialog();
} else {
_showFailDialog();
}
}
// 显示答对对话框
void _showSuccessDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('回答正确!'),
content: Text('恭喜你答对了这道题'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('继续'),
),
],
),
);
}
每道题提供3个渐进式提示,实现原理如下:
dart复制int _currentHintIndex = 0; // 当前提示索引
void _showHint() {
// 检查是否还有未显示的提示
if (_currentHintIndex < widget.question.hints.length) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('提示 ${_currentHintIndex + 1}'),
content: Text(widget.question.hints[_currentHintIndex]),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_currentHintIndex++;
});
},
child: Text('知道了'),
),
],
),
);
} else {
// 所有提示已显示完毕
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已经没有更多提示了')),
);
}
}
挑战模式会随机选择10道题,计时完成:
dart复制// 挑战状态变量
List<Question> _challengeQuestions = [];
int _currentIndex = 0;
int _score = 0;
bool _isStarted = false;
Duration _timeElapsed = Duration.zero;
Timer? _timer;
// 开始挑战
void _startChallenge() {
final random = Random();
setState(() {
// 随机选择10道题
_challengeQuestions = List.from(widget.questions)..shuffle(random);
_challengeQuestions = _challengeQuestions.take(10).toList();
_currentIndex = 0;
_score = 0;
_isStarted = true;
_timeElapsed = Duration.zero;
// 启动计时器
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_timeElapsed += Duration(seconds: 1);
});
});
});
}
// 提交挑战答案
void _submitChallengeAnswer(bool isCorrect) {
setState(() {
if (isCorrect) {
_score++;
}
// 移动到下一题或结束挑战
if (_currentIndex < _challengeQuestions.length - 1) {
_currentIndex++;
} else {
_endChallenge();
}
});
}
// 结束挑战
void _endChallenge() {
_timer?.cancel();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('挑战完成'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('得分: $_score/10'),
Text('用时: ${_timeElapsed.inSeconds}秒'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('确定'),
),
],
),
);
}
为不同难度题目设计颜色区分:
dart复制class DifficultyChip extends StatelessWidget {
final String difficulty;
const DifficultyChip({required this.difficulty});
Color _getDifficultyColor() {
switch (difficulty) {
case '简单':
return Colors.green;
case '中等':
return Colors.orange;
case '困难':
return Colors.red;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getDifficultyColor().withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
difficulty,
style: TextStyle(
color: _getDifficultyColor(),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
);
}
}
题目列表中的每一项使用Card组件呈现:
dart复制class QuestionCard extends StatelessWidget {
final Question question;
final VoidCallback? onTap;
const QuestionCard({required this.question, this.onTap});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DifficultyChip(difficulty: question.difficulty),
if (question.isAnswered)
Icon(
question.isCorrect ? Icons.check : Icons.close,
color: question.isCorrect ? Colors.green : Colors.red,
),
],
),
SizedBox(height: 8),
Text(
question.question,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
question.category,
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
),
),
),
);
}
}
答题页面包含题目、提示按钮和答案输入框:
dart复制@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('第${widget.question.id}题'),
actions: [
IconButton(
icon: Icon(Icons.lightbulb_outline),
onPressed: _showHint,
tooltip: '提示',
),
],
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.question.question,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 24),
TextField(
controller: _answerController,
decoration: InputDecoration(
labelText: '请输入答案',
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.send),
onPressed: _submitAnswer,
),
),
onSubmitted: (_) => _submitAnswer(),
),
SizedBox(height: 16),
if (widget.question.isAnswered)
Text(
widget.question.isCorrect ? '回答正确!' : '回答错误',
style: TextStyle(
color: widget.question.isCorrect ? Colors.green : Colors.red,
fontSize: 16,
),
),
],
),
),
);
}
实现本地存储服务:
dart复制import 'package:shared_preferences/shared_preferences.dart';
class StorageService {
static const String _answeredKey = 'answered_questions';
static const String _scoreKey = 'total_score';
Future<void> saveAnsweredQuestions(List<Question> questions) async {
final prefs = await SharedPreferences.getInstance();
final answered = questions.where((q) => q.isAnswered).toList();
final jsonList = answered.map((q) => q.toJson()).toList();
await prefs.setString(_answeredKey, json.encode(jsonList));
}
Future<List<Question>> loadAnsweredQuestions() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_answeredKey);
if (jsonStr == null) return [];
final List<dynamic> jsonList = json.decode(jsonStr);
return jsonList.map((json) => Question.fromJson(json)).toList();
}
Future<void> saveScore(int score) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_scoreKey, score);
}
Future<int> loadScore() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_scoreKey) ?? 0;
}
}
对于小型应用,使用setState完全可以满足需求。但随着功能复杂化,建议采用Provider:
dart复制class QuestionProvider extends ChangeNotifier {
List<Question> _questions = [];
String _selectedDifficulty = '全部';
List<Question> get questions => _questions;
String get selectedDifficulty => _selectedDifficulty;
List<Question> get filteredQuestions {
if (_selectedDifficulty == '全部') return _questions;
return _questions.where((q) => q.difficulty == _selectedDifficulty).toList();
}
void setDifficulty(String difficulty) {
_selectedDifficulty = difficulty;
notifyListeners();
}
void answerQuestion(Question question, bool isCorrect) {
question.isAnswered = true;
question.isCorrect = isCorrect;
notifyListeners();
}
}
在main.dart中配置Provider:
dart复制void main() {
runApp(
ChangeNotifierProvider(
create: (context) => QuestionProvider(),
child: MyApp(),
),
);
}
虽然Flutter应用在HarmonyOS上基本可以无缝运行,但仍有几点需要注意:
图标和启动图适配:
权限处理:
xml复制<!-- 在AndroidManifest.xml中添加HarmonyOS所需权限 -->
<uses-permission ohos:name="ohos.permission.INTERNET"/>
打包发布:
特定API兼容性:
使用ListView.builder实现懒加载:
dart复制ListView.builder(
itemCount: _filteredQuestions.length,
itemBuilder: (context, index) {
return QuestionCard(
question: _filteredQuestions[index],
onTap: () => _navigateToQuestionDetail(_filteredQuestions[index]),
);
},
)
如果应用包含图片资源,使用缓存策略:
dart复制CachedNetworkImage(
imageUrl: question.imageUrl,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
)
使用const构造函数和Provider的select方法减少不必要的重建:
dart复制// 使用const构造函数
class DifficultyChip extends StatelessWidget {
const DifficultyChip({required this.difficulty});
// ...
}
// 在build方法中使用
DifficultyChip(
difficulty: question.difficulty,
)
// 使用Provider的select方法
final questions = context.select<QuestionProvider, List<Question>>(
(provider) => provider.filteredQuestions
);
测试答案判断逻辑:
dart复制void main() {
group('答案判断测试', () {
test('答案正确应返回true', () {
final question = Question(
id: 1,
question: '什么东西越洗越脏?',
answer: '水',
category: '物品',
difficulty: '简单',
hints: [],
);
expect(checkAnswer(question, '水'), true);
expect(checkAnswer(question, '水'), true); // 大小写不敏感
});
test('答案错误应返回false', () {
final question = Question(
id: 1,
question: '什么东西越洗越脏?',
answer: '水',
category: '物品',
difficulty: '简单',
hints: [],
);
expect(checkAnswer(question, '肥皂'), false);
});
});
}
测试题目卡片组件:
dart复制void main() {
testWidgets('题目卡片显示测试', (WidgetTester tester) async {
final question = Question(
id: 1,
question: '什么东西越洗越脏?',
answer: '水',
category: '物品',
difficulty: '简单',
hints: [],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: QuestionCard(question: question),
),
),
);
expect(find.text('什么东西越洗越脏?'), findsOneWidget);
expect(find.text('简单'), findsOneWidget);
expect(find.text('物品'), findsOneWidget);
});
}
测试完整答题流程:
dart复制void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('完整答题流程测试', (WidgetTester tester) async {
// 启动应用
await tester.pumpWidget(MyApp());
// 验证首页显示
expect(find.text('脑筋急转弯'), findsOneWidget);
// 点击第一道题
await tester.tap(find.byType(QuestionCard).first);
await tester.pumpAndSettle();
// 输入答案并提交
await tester.enterText(find.byType(TextField), '水');
await tester.tap(find.byIcon(Icons.send));
await tester.pumpAndSettle();
// 验证结果
expect(find.text('回答正确'), findsOneWidget);
});
}
使用Dio实现网络请求:
dart复制import 'package:dio/dio.dart';
class QuestionApi {
final Dio _dio = Dio();
static const String _baseUrl = 'https://api.example.com';
Future<List<Question>> getQuestions() async {
try {
final response = await _dio.get('$_baseUrl/questions');
return (response.data as List)
.map((json) => Question.fromJson(json))
.toList();
} catch (e) {
throw Exception('获取题目失败: $e');
}
}
Future<Question> getRandomQuestion() async {
try {
final response = await _dio.get('$_baseUrl/questions/random');
return Question.fromJson(response.data);
} catch (e) {
throw Exception('获取随机题目失败: $e');
}
}
}
使用speech_to_text插件:
dart复制import 'package:speech_to_text/speech_to_text.dart' as stt;
class VoiceInputButton extends StatefulWidget {
final Function(String) onResult;
const VoiceInputButton({required this.onResult});
@override
_VoiceInputButtonState createState() => _VoiceInputButtonState();
}
class _VoiceInputButtonState extends State<VoiceInputButton> {
final stt.SpeechToText _speech = stt.SpeechToText();
bool _isListening = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(_isListening ? Icons.mic : Icons.mic_none),
color: _isListening ? Colors.red : null,
onPressed: _isListening ? _stopListening : _startListening,
);
}
Future<void> _startListening() async {
bool available = await _speech.initialize();
if (available) {
setState(() => _isListening = true);
_speech.listen(
onResult: (result) {
if (result.finalResult) {
widget.onResult(result.recognizedWords);
setState(() => _isListening = false);
}
},
);
}
}
void _stopListening() {
_speech.stop();
setState(() => _isListening = false);
}
}
使用share_plus插件:
dart复制import 'package:share_plus/share_plus.dart';
Future<void> shareAchievement(int score, double accuracy) async {
await Share.share(
'我在脑筋急转弯挑战中获得了$score分,正确率${accuracy.toStringAsFixed(1)}%!你也来试试吧!',
subject: '脑筋急转弯挑战成绩',
);
}
bash复制keytool -genkey -v -keystore ~/brain-teaser-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias brainteaser
code复制storePassword=your_password
keyPassword=your_password
keyAlias=brainteaser
storeFile=/path/to/brain-teaser-key.jks
gradle复制android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
bash复制flutter build apk --release
bash复制flutter build apk --release
在实际开发过程中,我总结了以下几点经验:
状态管理选择:对于小型应用,setState完全够用,不必过早引入复杂状态管理方案。当组件树较深或状态共享复杂时,再考虑Provider或Riverpod。
性能优化:Flutter性能已经很好,但仍需注意:
HarmonyOS适配:虽然Flutter应用在HarmonyOS上基本可以直接运行,但仍需:
测试策略:建议采用金字塔测试策略:
持续学习:Flutter生态发展迅速,建议:
这个项目完整展示了如何使用Flutter开发一个功能完善的跨平台应用,从架构设计到具体实现,从状态管理到性能优化,涵盖了实际开发中的各个环节。希望这份经验分享能帮助到正在学习Flutter的开发者们。