Flutter for OpenHarmony:构建一个 Flutter 颜色分类游戏,深入解析状态管理、游戏逻辑与声明式 UI 设计
Flutter for OpenHarmony:构建一个 Flutter 颜色分类游戏,深入解析状态管理、游戏逻辑与声明式 UI 设计
Flutter for OpenHarmony:构建一个 Flutter 颜色分类游戏,深入解析状态管理、游戏逻辑与声明式 UI 设计
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
发布时间:2026年2月6日
技术栈:Flutter 3.22+、Dart 3.4+、Material Design 3
适用读者:中级 Flutter 开发者、益智游戏爱好者、对状态驱动架构与算法思维感兴趣的技术人员
引言:从“试管排序”看逻辑思维的数字化表达
“颜色分类”(Color Sort)是近年来风靡移动端的益智游戏之一。玩家面对若干装有彩色球的试管,需通过将球在试管间移动,最终使每个非空试管仅包含同一种颜色的球。规则极简,却蕴含丰富的策略性——它本质上是一个受限状态空间搜索问题,考验玩家的规划能力、逆向思维与容错判断。
这类游戏的魅力在于:没有时间压力,没有随机干扰,胜负完全取决于逻辑推理。它不像动作游戏依赖反应速度,而更像数独或华容道,是大脑的“健身房”。
今天,我们将用 Flutter 从零构建一个完整的“颜色分类”游戏。这个项目虽无复杂动画或物理引擎,却完美展示了 如何用纯 Dart 代码实现严谨的游戏逻辑、安全的状态管理与直观的用户交互。
更重要的是,它揭示了 Flutter 在构建高内聚、低耦合、可测试的交互式应用方面的强大能力——即使是最“静态”的益智游戏,也能通过声明式 UI 与响应式状态流,带来流畅愉悦的体验。
一、游戏机制与核心规则
基本设定
- 初始状态:6 个试管,每管 4 个彩色球(共 6 种颜色 × 4 球 = 24 球)
- 辅助空间:额外提供 2 个空试管,用于临时存放
- 目标状态:所有非空试管均为单一颜色(即“完成”状态)
- 操作规则:
- 只能移动顶部球
- 只能移入非满试管
- 只能移入空试管或顶部颜色相同的试管
技术映射
| 游戏概念 | 代码实体 | 数据结构 |
|---|---|---|
| 试管 | Tube 类 |
List<Color> |
| 彩色球 | Color 对象 |
Flutter 内置类型 |
| 移动操作 | _handleTap |
状态变更函数 |
| 胜利判定 | _checkWin |
布尔谓词 |
这种对象化建模使游戏逻辑清晰、可维护、可扩展。
二、数据模型设计:Tube 类的封装艺术
核心定义
class Tube {
final List<Color> balls;
static const int capacity = 4;
Tube(this.balls);
bool get isEmpty => balls.isEmpty;
bool get isFull => balls.length == capacity;
bool get isComplete => balls.length == capacity && balls.every((b) => b == balls[0]);
Color? get topColor => balls.isEmpty ? null : balls.last;
void push(Color color) { if (!isFull) balls.add(color); }
Color pop() { return balls.removeLast(); }
}

设计亮点
1. 不可变容量
static const int capacity = 4;
使用 static const 定义全局常量,确保所有试管行为一致,且编译期优化。
2. 语义化 Getter
isEmpty/isFull:封装长度判断,提升可读性isComplete:核心胜利条件,一行代码表达“全同色且满”topColor:安全返回顶部球颜色(空时为null)
3. 安全栈操作
push自动检查容量,防止越界pop返回被移除的球,便于撤销或记录
✅ 面向对象原则:
Tube封装了所有与“试管”相关的状态与行为,外部无需关心内部实现。
三、游戏初始化:随机但可解的谜题生成
关键步骤
void _newGame() {
// 1. 创建24个球(6色×4)
List<Color> allBalls = [];
for (var color in colorPalette) {
allBalls.addAll(List.filled(4, color));
}
// 2. 打乱顺序
allBalls.shuffle(_random);
// 3. 分成6管
tubes = [];
for (int i = 0; i < 6; i++) {
tubes.add(Tube(allBalls.sublist(i * 4, (i + 1) * 4)));
}
// 4. 添加2个空管
tubes.add(Tube([]));
tubes.add(Tube([]));
}

为什么这样设计?
- 确定性输入:先创建完整球集,再打乱,确保颜色数量严格平衡
- 可解性保障:虽然未实现“验证可解”的算法,但随机打乱+足够空管(2个)在实践中几乎总可解
- 扩展友好:修改
colorPalette即可支持更多颜色(如 8 色 → 8 管 + 2 空管)
💡 进阶思考:专业实现应加入“可解性验证”(如 BFS 检查初始状态是否可达目标),但对休闲游戏非必需。
四、交互逻辑:双击模型与状态机
用户操作流程
[空闲]
│
▼ (点击非空试管)
[选中状态] ——(点击自身)——→ [空闲](取消)
│
▼ (点击有效目标试管)
[执行移动] ——(检查胜利)——→ [弹出胜利对话框]
│
▼ (点击无效试管)
[重置为空闲]
核心函数 _handleTap
void _handleTap(int index) {
if (selectedTubeIndex == null) {
// 第一次点击:选中
if (!tubes[index].isEmpty) {
selectedTubeIndex = index;
}
} else {
if (index == selectedTubeIndex!) {
// 点击自身:取消
selectedTubeIndex = null;
} else if (_canMove(selectedTubeIndex!, index)) {
// 有效移动
final color = tubes[selectedTubeIndex!].pop();
tubes[index].push(color);
selectedTubeIndex = null;
if (_checkWin()) _showWinDialog();
} else {
// 无效操作:取消选择
selectedTubeIndex = null;
}
}
setState(() {});
}

状态管理哲学
- 单一状态变量:
selectedTubeIndex(int?)编码全部交互状态 - 无中间状态:每次点击后立即重置或提交,避免“悬停”状态
- 即时反馈:无效操作直接取消选择,而非提示错误(降低挫败感)
五、移动验证:_canMove 的逻辑严谨性
规则实现
bool _canMove(int fromIndex, int toIndex) {
if (fromIndex == toIndex) return false; // 不能自移
final from = tubes[fromIndex];
final to = tubes[toIndex];
if (from.isEmpty) return false; // 源空
if (to.isFull) return false; // 目标满
if (to.isEmpty) return true; // 目标空 → 允许
return from.topColor == to.topColor; // 同色 → 允许
}
边界覆盖
| 情况 | 是否允许 | 代码处理 |
|---|---|---|
| 源 == 目标 | ❌ | fromIndex == toIndex |
| 源为空 | ❌ | from.isEmpty |
| 目标满 | ❌ | to.isFull |
| 目标空 | ✅ | to.isEmpty |
| 目标非空但同色 | ✅ | topColor == topColor |
| 目标非空且异色 | ❌ | 最后一行返回 false |
✅ 防御性编程:所有非法情况显式排除,逻辑无歧义。
六、胜利判定:_checkWin 的简洁之美
实现
bool _checkWin() {
return tubes.every((tube) => tube.isEmpty || tube.isComplete);
}
函数式思维
- 使用
every高阶函数,表达“所有试管满足条件” - 条件为
isEmpty || isComplete,覆盖空管与完成管
💡 对比传统写法:
for (var tube in tubes) { if (!tube.isEmpty && !tube.isComplete) return false; } return true;函数式版本更简洁、声明式、不易出错。
七、UI 架构:GridView + 声明式渲染
试管渲染
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemCount: tubes.length,
itemBuilder: (context, index) {
final tube = tubes[index];
final isSelected = selectedTubeIndex == index;
return GestureDetector(
onTap: () => _handleTap(index),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: isSelected ? Colors.indigo : Colors.grey.shade400,
width: isSelected ? 3 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
...List.generate(Tube.capacity, (i) {
final ballIndex = tube.balls.length - 1 - i;
if (ballIndex < 0) {
return const Spacer();
} else {
return Container(/* 彩色球 */);
}
}),
],
),
),
);
},
)
视觉设计要点
- 底部对齐:
mainAxisAlignment: MainAxisAlignment.end模拟真实试管 - 动态边框:选中时加粗变色(
Colors.indigo),提供明确反馈 - 球体堆叠:通过
List.generate(4)生成固定高度槽位,空位用Spacer填充 - 色彩系统:使用
colorPalette预定义色,保证视觉一致性
🎨 Material 3 整合:
ColorScheme.fromSeed(seedColor: Colors.indigo)自动生成协调的主题色。
八、性能与可访问性优化
性能关键点
- 最小化 rebuild:仅
tubes和selectedTubeIndex触发更新 - 高效列表操作:
List.generate+ 条件渲染,避免不必要的 widget 创建 - 常量优化:
const修饰SliverGridDelegate、EdgeInsets等
可访问性(Accessibility)
- 大点击区域:每个试管 ≥ 80×120 dp,符合触控规范
- 高对比度:深灰边框 vs 白色背景,彩色球鲜明易辨
- 语义说明:顶部文字清晰描述游戏规则,辅助新用户
九、扩展方向:从休闲游戏到教育工具
当前实现是一个优秀的 MVP,但可进一步升级:
1. 难度分级
- 初级:4 色 × 4 球 + 2 空管
- 高级:8 色 × 5 球 + 2 空管
- 专家:10 色 + 仅 1 空管
2. 撤销/重做
- 记录操作历史(
List<Move>) - 支持无限步撤销
3. 提示系统
- 高亮一个有效移动
- 显示“最少步数”估计(需 A* 算法)
4. 成就与统计
- “首次通关”、“无提示完成”等徽章
- 平均步数、成功率统计
5. 教育模式
- 针对儿童:使用动物/水果图标代替颜色
- 教学关卡:逐步引入规则
十、总结:益智游戏中的工程智慧
这个“颜色分类”游戏证明了:伟大的用户体验,往往源于对基础逻辑的极致打磨。
通过:
- 严谨的对象建模(
Tube类) - 清晰的状态机(选中/移动/重置)
- 声明式的 UI 渲染(
GridView+ 条件样式) - 函数式的胜利判定(
every+ 谓词)
我们得以在 不到 200 行 Dart 代码 内,构建一个逻辑严密、交互流畅、视觉清晰的益智游戏。
更重要的是,它展示了 Flutter 的核心优势:用统一的声明式范式,构建从简单表单到复杂游戏的全谱系应用。
无论你是开发电商 App、企业后台,还是休闲游戏,这一原则始终适用——状态驱动 UI,逻辑驱动体验。
附录:动手实验建议
- 添加音效:移动球时播放“滴”声,胜利时播放欢呼
- 实现撤销功能:记录每一步操作,支持回退
- 动态难度:根据用户水平自动调整颜色数量
- 本地存储:保存最佳步数记录
- 主题切换:支持深色模式、节日皮肤(如圣诞红绿球)
🌟 Happy Coding with Flutter!
愿你的每一行代码,都如一个彩色球般精准归位;每一次交互,都带来逻辑的愉悦与思维的满足。
更多推荐



所有评论(0)