Flutter三方库适配OpenHarmony【rock_paper_scissors】猜拳小游戏项目完整实战
Flutter三方库适配OpenHarmony【rock_paper_scissors】猜拳小游戏项目完整实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
rock_paper_scissors 是一个经典的石头、剪刀、布 Flutter 小游戏。玩家从 rock、paper、scissors 三个选项中选择一个,电脑通过随机数生成出拳,应用延迟 500ms 后判断胜负,并分别统计玩家胜场、电脑胜场和平局次数。
这个项目虽然规则简单,但很适合练习 Flutter 的 状态管理、随机数、延迟反馈、条件渲染、统计卡片、GestureDetector 点击、Emoji 文本展示和 OpenHarmony 触摸适配。同时,它也暴露了一些真实工程边界,例如 _isPlaying 只被赋值但没有参与防重复点击,延迟回调没有 mounted 保护,连续点击可能叠加多次判定。

图示说明:本文围绕 Flutter 工程中的 rock_paper_scissors 项目展开,重点分析出拳数据结构、随机电脑选择、胜负判断、比分统计、UI 布局和 OpenHarmony 适配关注点。
猜拳小游戏的关键不是规则本身,而是“玩家选择、电脑选择、延迟揭晓、结果统计”这一轮交互是否清晰稳定。
本文将基于项目真实源码展开,核心内容包括:
RockPaperScissorsApp的应用入口与深紫色 Material 3 主题_choices如何定义 rock、paper、scissors 三个选项_play()如何生成电脑随机出拳并进入本轮状态_determineWinner()如何判断胜负和平局_buildStatCard()如何复用比分统计卡片_getChoiceIcon()如何把出拳名称转换成 Emoji- UI 如何展示玩家、VS、电脑和结果
_isPlaying未参与点击锁定的真实边界- OpenHarmony 适配时需要验证的 Emoji、点击和延迟回调
一、项目背景与目标
1.1 游戏定位
rock_paper_scissors 是一个轻量猜拳游戏。玩家点击底部三个选项之一后,电脑随机选择一个选项,页面展示双方出拳,并在短暂延迟后给出结果。
从玩家视角看,流程是:
- 打开应用,看到比分和三个出拳选项。
- 点击 rock、paper 或 scissors。
- 页面显示玩家选择和电脑选择。
- 500ms 后显示本轮结果。
- 比分卡片同步更新。
- 玩家可以继续下一轮。
从工程视角看,流程是:
- 定义三种出拳数据。
- 玩家点击触发
_play()。 - 随机生成电脑选择。
- 写入玩家和电脑出拳状态。
- 延迟 500ms 调用胜负判断。
- 根据结果更新比分。
- UI 根据状态重新渲染。
1.2 当前功能概览
| 功能 | 当前实现 | 技术点 |
|---|---|---|
| 应用入口 | runApp(const RockPaperScissorsApp()) |
Flutter 启动流程 |
| 应用主题 | 深紫色 Material 3 | ColorScheme.fromSeed |
| 出拳选项 | rock / paper / scissors | List<Map<String, String>> |
| 电脑出拳 | 随机 0 到 2 | math.Random() |
| 胜负判断 | 平局、玩家赢、电脑赢 | 条件分支 |
| 延迟揭晓 | 500ms 后判定 | Future.delayed |
| 比分统计 | You / Draws / Computer | 三个 Card |
| 图标展示 | Emoji 文本 | rock、paper、scissors 图标 |
| 点击交互 | GestureDetector |
底部三选项 |
1.3 适合学习的能力
这个项目适合学习:
- 小游戏状态建模
- 随机选择逻辑
- 条件分支胜负判断
- 延迟反馈设计
- 统计卡片复用
- GestureDetector 点击事件
- Emoji 文本渲染
- OpenHarmony 下触摸与字体表现验证
二、环境准备与工程结构
2.1 技术栈概览
项目使用 Flutter SDK 和 Dart 标准库能力,没有复杂插件。
| 类别 | 当前使用 | 说明 |
|---|---|---|
| 开发语言 | Dart | Flutter 应用主语言 |
| UI 框架 | Flutter Material | 页面、卡片、文本、容器 |
| 随机数 | dart:math |
电脑随机出拳 |
| 状态管理 | StatefulWidget + setState |
游戏状态刷新 |
| 点击输入 | GestureDetector |
玩家出拳 |
| 延迟反馈 | Future.delayed |
延迟判定结果 |
| 图标显示 | Emoji 文本 | 出拳视觉表达 |
| 目标适配 | 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
当前项目虽然使用 Emoji 作为出拳图标,但仍然需要 Material 组件和 Material Icons 支持基础页面。
2.3 主源码结构
核心代码集中在 lib/main.dart:
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(const RockPaperScissorsApp());
}
主要结构如下:
| 结构 | 类型 | 作用 |
|---|---|---|
RockPaperScissorsApp |
StatelessWidget |
应用根组件 |
RockPaperScissorsHomePage |
StatefulWidget |
猜拳首页 |
_RockPaperScissorsHomePageState |
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 RockPaperScissorsApp());
}
runApp 会把根组件挂载到 Flutter 渲染树。
3.2 RockPaperScissorsApp 根组件
根组件代码如下:
class RockPaperScissorsApp extends StatelessWidget {
const RockPaperScissorsApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rock Paper Scissors',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const RockPaperScissorsHomePage(title: 'Rock Paper Scissors'),
);
}
}
它完成了:
- 设置应用标题为
Rock Paper Scissors。 - 使用深紫色作为 Material 3 种子色。
- 将首页设置为
RockPaperScissorsHomePage。
3.3 深紫色主题的意义
深紫色让小游戏界面更有娱乐感,也和被选中的底部选项颜色一致:
Colors.deepPurple.shade100
Colors.deepPurple
主题色与选中态保持一致,可以让用户更容易理解当前选择。
四、游戏状态设计
4.1 状态字段
页面状态如下:
String _playerChoice = '';
String _computerChoice = '';
String _result = '';
int _playerWins = 0;
int _computerWins = 0;
int _draws = 0;
bool _isPlaying = false;
字段含义如下:
| 字段 | 作用 |
|---|---|
_playerChoice |
玩家当前选择 |
_computerChoice |
电脑当前选择 |
_result |
本轮结果文案 |
_playerWins |
玩家胜场 |
_computerWins |
电脑胜场 |
_draws |
平局次数 |
_isPlaying |
当前源码中被设置,但没有实际参与 UI 控制 |
4.2 结果状态
_result 当前可能是:
| 值 | 含义 |
|---|---|
| 空字符串 | 尚未有结果 |
Draw! |
平局 |
You Win! |
玩家获胜 |
You Lose! |
玩家失败 |
页面会根据结果文字包含的关键词决定颜色。
4.3 _isPlaying 的真实边界
源码中 _play() 会把 _isPlaying 设置为 true:
_isPlaying = true;
但后续没有任何 UI 或逻辑使用 _isPlaying 来禁用点击,也没有在判定结束后把它重置为 false。因此当前这个字段没有实际控制作用。
如果要防止连续点击,它应该参与按钮禁用逻辑。
五、出拳数据结构
5.1 _choices 列表
三种出拳定义如下:
final List<Map<String, String>> _choices = [
{'name': 'rock', 'icon': '🪨', 'beats': 'scissors'},
{'name': 'paper', 'icon': '📄', 'beats': 'rock'},
{'name': 'scissors', 'icon': '✂️', 'beats': 'paper'},
];
每个选项包含:
| 字段 | 作用 |
|---|---|
name |
出拳名称 |
icon |
出拳 Emoji |
beats |
当前选项能击败的选项 |
5.2 数据结构优点
这个结构可读性很好:
- rock 使用石头 Emoji。
- paper 使用纸张 Emoji。
- scissors 使用剪刀 Emoji。
- beats 字段表达规则关系。
5.3 beats 字段的当前边界
虽然 _choices 中定义了 beats,但实际胜负判断没有使用这个字段,而是硬编码条件:
(player == 'rock' && computer == 'scissors')
如果要让规则更易扩展,可以直接使用 beats 字段判断。
5.4 获取图标方法
出拳名称转换图标使用:
String _getChoiceIcon(String choice) {
return _choices.firstWhere((c) => c['name'] == choice)['icon']!;
}
它会在 _choices 中查找对应选项,并返回 Emoji。
六、玩家出拳与电脑随机选择
6.1 _play 方法
玩家点击选项后调用:
void _play(String choice) {
final random = math.Random();
final computerChoiceIndex = random.nextInt(3);
final computerChoice = _choices[computerChoiceIndex]['name']!;
setState(() {
_playerChoice = choice;
_computerChoice = computerChoice;
_isPlaying = true;
});
Future.delayed(const Duration(milliseconds: 500), () {
_determineWinner(choice, computerChoice);
});
}
这段代码做了:
- 创建随机数对象。
- 生成 0 到 2 的随机索引。
- 从
_choices中取电脑选择。 - 更新玩家和电脑选择。
- 500ms 后判定胜负。
6.2 随机索引范围
电脑出拳索引来自:
random.nextInt(3)
返回值可能是 0、1、2,对应 rock、paper、scissors。
6.3 延迟反馈设计
延迟 500ms 的代码如下:
Future.delayed(const Duration(milliseconds: 500), () {
_determineWinner(choice, computerChoice);
});
这让页面先展示双方选择,再出现结果,游戏反馈更自然。
6.4 延迟回调边界
当前延迟回调没有 mounted 判断。如果页面在 500ms 内被销毁,回调里调用 setState 可能出现问题。
更稳妥的写法是:
Future.delayed(const Duration(milliseconds: 500), () {
if (!mounted) return;
_determineWinner(choice, computerChoice);
});
这能避免页面销毁后的状态更新。
七、胜负判断逻辑
7.1 _determineWinner 方法
胜负判断如下:
void _determineWinner(String player, String computer) {
setState(() {
if (player == computer) {
_result = 'Draw!';
_draws++;
} else if (
(player == 'rock' && computer == 'scissors') ||
(player == 'paper' && computer == 'rock') ||
(player == 'scissors' && computer == 'paper')
) {
_result = 'You Win!';
_playerWins++;
} else {
_result = 'You Lose!';
_computerWins++;
}
});
}
7.2 平局判断
当玩家和电脑选择相同:
if (player == computer)
结果是 Draw!,平局次数加 1。
7.3 玩家胜利判断
玩家胜利条件有三种:
| 玩家 | 电脑 | 结果 |
|---|---|---|
| rock | scissors | 玩家赢 |
| paper | rock | 玩家赢 |
| scissors | paper | 玩家赢 |
源码中使用条件组合表达:
(player == 'rock' && computer == 'scissors') ||
(player == 'paper' && computer == 'rock') ||
(player == 'scissors' && computer == 'paper')
7.4 电脑胜利判断
如果不是平局,也不是玩家胜利,就进入电脑胜利分支:
else {
_result = 'You Lose!';
_computerWins++;
}
这个逻辑简洁且符合三选项规则。
八、比分统计卡片
8.1 顶部统计区
顶部有三个统计卡片:
_buildStatCard('You', _playerWins, Colors.green)
_buildStatCard('Draws', _draws, Colors.grey)
_buildStatCard('Computer', _computerWins, Colors.red)
分别展示玩家胜场、平局次数和电脑胜场。
8.2 _buildStatCard 方法
统计卡片方法如下:
Widget _buildStatCard(String label, int value, Color color) {
return Card(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Column(
children: [
Text(label, style: TextStyle(color: color)),
const SizedBox(height: 4),
Text(
value.toString(),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
8.3 颜色语义
| 卡片 | 颜色 | 含义 |
|---|---|---|
| You | 绿色 | 玩家胜利 |
| Draws | 灰色 | 平局 |
| Computer | 红色 | 电脑胜利 |
8.4 统计持久化边界
当前比分只存在内存中。应用重启后玩家胜场、电脑胜场和平局都会恢复为 0。
如果需要长期记录,需要引入本地存储。
九、对战展示区
9.1 玩家选择区域
玩家选择区域显示:
Text(
_playerChoice.isEmpty ? '?' : _getChoiceIcon(_playerChoice),
style: const TextStyle(fontSize: 48),
)
首次进入时显示问号,玩家选择后显示对应 Emoji。
9.2 电脑选择区域
电脑区域逻辑相同:
Text(
_computerChoice.isEmpty ? '?' : _getChoiceIcon(_computerChoice),
style: const TextStyle(fontSize: 48),
)
电脑选择会在玩家点击后立即显示。
9.3 VS 和结果
中间区域展示 VS 和结果:
Text(
_result.isEmpty ? '' : _result,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _result.contains('Win')
? Colors.green
: _result.contains('Lose')
? Colors.red
: Colors.grey,
),
)
结果颜色根据文本判断:
| 结果 | 颜色 |
|---|---|
| You Win! | 绿色 |
| You Lose! | 红色 |
| Draw! | 灰色 |
9.4 Emoji 字体边界
项目使用了石头、纸张和剪刀 Emoji。如果目标设备字体不完整,可能出现方框、黑白图标或显示差异。
OpenHarmony 设备上需要特别验证 Emoji 渲染效果。
十、底部出拳按钮
10.1 三个选项按钮
底部按钮由 _choices.map() 生成:
children: _choices.map((choice) {
return GestureDetector(
onTap: () => _play(choice['name']!),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(...),
child: Column(
children: [
Text(choice['icon']!, style: const TextStyle(fontSize: 48)),
const SizedBox(height: 4),
Text(choice['name']!.toUpperCase()),
],
),
),
);
}).toList()
这让三个按钮结构一致。
10.2 选中态样式
当前玩家选择会被高亮:
color: _playerChoice == choice['name']
? Colors.deepPurple.shade100
: Colors.grey.shade100
边框也会同步变化:
border: Border.all(
color: _playerChoice == choice['name']
? Colors.deepPurple
: Colors.transparent,
width: 2,
)
10.3 点击区域
每个按钮由 GestureDetector 包裹。用户点击整个容器都能触发出拳。
10.4 连续点击边界
当前 _isPlaying 没有阻止连续点击。玩家可以在 500ms 延迟期间快速点击多次,每次都会安排一个 _determineWinner() 回调,最终可能导致比分多次更新。
更稳妥的做法是:
if (_isPlaying) return;
并在判定结束后重置:
_isPlaying = false;
十一、规则结构优化
11.1 使用 beats 字段判断
当前 _choices 已经有 beats 字段:
{'name': 'rock', 'icon': '🪨', 'beats': 'scissors'}
可以用它简化胜负判断。
11.2 查找玩家选择
可以先找到玩家选项:
final playerChoice = _choices.firstWhere((item) => item['name'] == player);
然后判断:
if (player == computer) {
// draw
} else if (playerChoice['beats'] == computer) {
// player wins
} else {
// computer wins
}
11.3 优化价值
这种写法的好处是:
- 规则集中在数据表。
- 判断逻辑更短。
- 后续扩展选项更容易。
- 减少硬编码条件。
11.4 扩展玩法可能
如果增加 rock-paper-scissors-lizard-spock 之类扩展规则,数据驱动写法会比硬编码分支更容易维护。
十二、OpenHarmony 适配要点
12.1 适配关注范围
rock_paper_scissors 没有平台插件,重点验证 UI、触摸、Emoji 和延迟状态。
| 适配项 | 涉及源码 | 验证重点 |
|---|---|---|
| MaterialApp | 根组件 | 应用启动和主题 |
| Card | 比分统计 | 布局、阴影 |
| GestureDetector | 出拳点击 | 点击区域和响应 |
| Emoji Text | 三种出拳图标 | 字体兼容 |
| Future.delayed | 延迟判定 | 回调稳定性 |
| Row / Column | 页面布局 | 小屏是否溢出 |
| Container | 选中态 | 背景和边框 |
| Text | 结果颜色 | Win/Lose/Draw 状态 |
12.2 Emoji 渲染验证
源码使用:
'🪨'
'📄'
'✂️'
OpenHarmony 设备上需要确认:
- Emoji 是否正常显示。
- Emoji 是否有彩色字体支持。
- 字体大小是否稳定。
- 不同设备显示是否差异过大。
12.3 点击体验验证
底部出拳依赖:
GestureDetector(
onTap: () => _play(choice['name']!),
)
需要验证:
- 三个按钮都能点击。
- 选中态是否更新。
- 快速点击是否会重复计分。
- 小屏幕上按钮是否挤压。
12.4 延迟回调验证
结果判定有 500ms 延迟:
Future.delayed(const Duration(milliseconds: 500), () {
_determineWinner(choice, computerChoice);
});
需要验证:
- 点击后是否先显示选择。
- 500ms 后是否显示结果。
- 连续点击时比分是否符合预期。
- 页面退出时是否避免 setState 风险。
十三、测试与验证
13.1 静态分析
建议执行:
flutter analyze
重点关注:
_isPlaying是否未被有效使用。- 延迟回调是否缺少
mounted判断。 - Emoji 字符是否正常编码。
- UI 是否有小屏溢出风险。
13.2 组件测试方向
可以执行:
flutter test
适合覆盖的行为包括:
- 初始比分为 0。
- 页面显示 rock、paper、scissors 三个选项。
- 点击任意选项后玩家区域不再显示问号。
- 延迟后结果不为空。
- 玩家、电脑或平局统计会增加。
13.3 示例测试代码
下面是一段基础页面测试思路:
testWidgets('shows rock paper scissors options', (tester) async {
await tester.pumpWidget(const RockPaperScissorsApp());
expect(find.text('ROCK'), findsOneWidget);
expect(find.text('PAPER'), findsOneWidget);
expect(find.text('SCISSORS'), findsOneWidget);
});
这能确认三个出拳选项正常渲染。
13.4 延迟结果测试思路
可以等待 500ms 后检查结果区域:
testWidgets('tap option produces result', (tester) async {
await tester.pumpWidget(const RockPaperScissorsApp());
await tester.tap(find.text('ROCK'));
await tester.pump(const Duration(milliseconds: 600));
expect(
find.byWidgetPredicate((widget) {
return widget is Text &&
(widget.data == 'You Win!' ||
widget.data == 'You Lose!' ||
widget.data == 'Draw!');
}),
findsOneWidget,
);
});
由于电脑出拳随机,测试不应固定期望某一个结果。
13.5 手动验证流程
手动验证可以按如下顺序进行:
- 启动应用,确认三个比分均为 0。
- 点击 ROCK,确认玩家选择显示石头。
- 等待 500ms,确认出现结果。
- 连续点击多个选项,观察是否出现重复计分。
- 检查 You、Draws、Computer 三个统计是否更新。
- 在 OpenHarmony 设备上验证 Emoji 是否正常显示。
- 在小屏幕上观察三个出拳按钮是否溢出。
十四、常见问题与优化建议
14.1 为什么 _isPlaying 没有阻止重复点击
因为源码只设置了:
_isPlaying = true;
但没有在 _play() 开头判断它,也没有在判定结束后重置。所以它当前没有实际控制效果。
14.2 为什么连续点击可能多次计分
每次点击都会安排一个延迟回调:
Future.delayed(const Duration(milliseconds: 500), () {
_determineWinner(choice, computerChoice);
});
连续点击多次,就会有多个回调依次执行,比分也可能增加多次。
14.3 为什么 beats 字段没有被使用
_choices 中定义了:
'beats': 'scissors'
但胜负判断里没有读取这个字段,而是使用硬编码条件。它可以作为后续重构点。
14.4 为什么没有 Reset 按钮
当前源码没有提供清空比分的按钮。应用重启会清零,但运行中不能手动重置。可以在 AppBar 添加 reset action。
14.5 Emoji 显示异常怎么办
如果目标设备 Emoji 显示不稳定,可以改为:
- 使用 Material Icons。
- 使用本地图片资源。
- 使用自定义 SVG。
- 使用文字标签替代。
十五、工程扩展方向
15.1 增加防重复点击
可以在 _play() 开头加入:
if (_isPlaying) return;
判定结束后:
_isPlaying = false;
这样能确保一轮结束前不会重复计分。
15.2 增加 mounted 判断
延迟回调可以改成:
Future.delayed(const Duration(milliseconds: 500), () {
if (!mounted) return;
_determineWinner(choice, computerChoice);
});
这能避免页面销毁后继续更新状态。
15.3 使用 beats 字段重构规则
可以让胜负判断变成数据驱动:
final playerChoice = _choices.firstWhere((item) => item['name'] == player);
final playerBeatsComputer = playerChoice['beats'] == computer;
再根据结果更新比分。
15.4 增加比分重置
可以添加:
void _resetScore() {
setState(() {
_playerWins = 0;
_computerWins = 0;
_draws = 0;
_result = '';
_playerChoice = '';
_computerChoice = '';
});
}
然后绑定到 AppBar 的按钮。
15.5 增加对局历史
可以记录每一轮:
class RoundRecord {
const RoundRecord({
required this.player,
required this.computer,
required this.result,
});
final String player;
final String computer;
final String result;
}
有了历史记录,就可以展示最近 N 轮对局。
十六、完整核心代码回顾
16.1 应用入口
void main() {
runApp(const RockPaperScissorsApp());
}
入口负责启动根组件。
16.2 根组件
class RockPaperScissorsApp extends StatelessWidget {
const RockPaperScissorsApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rock Paper Scissors',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const RockPaperScissorsHomePage(title: 'Rock Paper Scissors'),
);
}
}
根组件负责主题和首页绑定。
16.3 出拳数据
final List<Map<String, String>> _choices = [
{'name': 'rock', 'icon': '🪨', 'beats': 'scissors'},
{'name': 'paper', 'icon': '📄', 'beats': 'rock'},
{'name': 'scissors', 'icon': '✂️', 'beats': 'paper'},
];
三种出拳都包含名称、图标和能击败的对象。
16.4 玩家出拳
void _play(String choice) {
final random = math.Random();
final computerChoiceIndex = random.nextInt(3);
final computerChoice = _choices[computerChoiceIndex]['name']!;
setState(() {
_playerChoice = choice;
_computerChoice = computerChoice;
_isPlaying = true;
});
Future.delayed(const Duration(milliseconds: 500), () {
_determineWinner(choice, computerChoice);
});
}
这段代码负责玩家出拳、电脑随机出拳和延迟判定。
16.5 胜负判定
if (player == computer) {
_result = 'Draw!';
_draws++;
} else if (
(player == 'rock' && computer == 'scissors') ||
(player == 'paper' && computer == 'rock') ||
(player == 'scissors' && computer == 'paper')
) {
_result = 'You Win!';
_playerWins++;
} else {
_result = 'You Lose!';
_computerWins++;
}
这是游戏规则的核心。
16.6 图标映射
String _getChoiceIcon(String choice) {
return _choices.firstWhere((c) => c['name'] == choice)['icon']!;
}
这个方法把出拳名称映射为 Emoji。
总结
rock_paper_scissors 用 Flutter 实现了一个完整的猜拳小游戏:它通过 _choices 定义 rock、paper、scissors 三种出拳,通过 _play() 处理玩家点击和电脑随机选择,通过 Future.delayed 做 500ms 延迟反馈,通过 _determineWinner() 判断胜负,并用三个统计卡片展示玩家胜场、平局和电脑胜场。
从 OpenHarmony 适配角度看,这个项目覆盖了 Material 主题、Card 统计、GestureDetector 触摸、Emoji 文本、Row/Column 布局、Container 选中态和延迟回调等基础能力,很适合验证 Flutter 小游戏页面在 OpenHarmony 上的表现。
当前源码也有几个真实边界:_isPlaying 没有真正参与防重复点击;连续点击可能叠加多个延迟判定并多次计分;延迟回调没有 mounted 判断;beats 字段已定义但未用于规则判断;运行中没有比分重置按钮。这些边界不影响项目作为入门实战案例使用,但在继续工程化时需要优先处理防重复点击和延迟回调安全性。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 官网:https://www.openharmony.cn
- OpenHarmony 文档:https://docs.openharmony.cn
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Flutter 官网:https://flutter.dev
- Flutter 文档:https://docs.flutter.dev
更多推荐



所有评论(0)