Flutter + OpenHarmony 实战:构建清晰、健壮的三屏状态流转

个人主页:ujainu

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

文章目录

#前言

在任何交互式应用中,状态管理页面导航是决定用户体验流畅度的核心。尤其在小游戏场景下,若处理不当,极易出现:

  • 游戏结束后仍残留定时器(CPU 占用不降)
  • 用户误触返回键回到"已结束"的游戏界面
  • 多次进入游戏导致控制器重复创建(内存泄漏)
  • 页面栈混乱,无法正确退出或重启

本文将围绕 MainMenuScreen → GameScreen → GameOverScreen 这一经典三段式流程,系统性地解决上述问题,实现:

状态隔离 :游戏结束时自动清理所有资源(控制器、动画、定时器)

路由优化 :使用 Navigator.pushReplacement 避免返回栈堆积

架构清晰 :通过状态机思想组织页面跳转逻辑

OpenHarmony 兼容:不依赖特定平台 API,纯 Flutter 实现

💡 核心理念页面即状态,状态变更驱动路由


一、为什么需要"游戏状态机"?

"状态机"听起来高深,其实本质很简单:一个游戏只能处于一种主状态,如:

  • MainMenu(主菜单)
  • Playing(游戏中)
  • GameOver(游戏结束)

这些状态互斥,且有明确的转换规则:

复制代码
MainMenu 
   └──[开始游戏]──→ Playing 
                     └──[失败/胜利]──→ GameOver
                                       └──[重新开始]──→ MainMenu(或直接新游戏)

若不用状态机,开发者常犯的错误包括:

  • GameScreen 中监听全局事件,即使已跳转到 GameOver 仍在运行
  • Navigator.push 层层叠加页面,导致用户按返回键时"穿越回过去的游戏"
  • 忘记 dispose 控制器,造成内存泄漏和逻辑错乱

因此,我们必须将 页面路由生命周期管理 深度绑定。


二、路由策略:为何选用 pushReplacement

方法 行为 适用场景
Navigator.push 将新页面压入栈顶 需要返回上一页(如设置页)
Navigator.pushReplacement 替换当前页,旧页被销毁 状态不可逆切换(如开始游戏、游戏结束)

在我们的三屏流程中:

  • 从主菜单 开始游戏 → 应销毁 MainMenuScreen,避免返回
  • 游戏结束 → 应销毁 GameScreen,防止残留逻辑
  • 从结束页 重新开始 → 应回到主菜单或直接新游戏,不应保留 GameOverScreen

因此,全部使用 pushReplacement 是最佳实践。

dart 复制代码
// 开始游戏:替换主菜单
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => const GameScreen()),
);

// 游戏结束:替换游戏界面
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => GameOverScreen(score: _score)),
);

✅ 效果:用户无法通过返回键"回到游戏中",彻底杜绝状态污染。


三、GameScreen:资源管理与生命周期

GameScreen 是资源密集型页面,必须在其销毁时清理一切。

关键组件:

  • AnimationController(控制球体运动)
  • Timer(倒计时或帧更新)
  • StreamSubscription(如有传感器数据)

正确做法:重写 dispose()

dart 复制代码
class GameScreen extends StatefulWidget {
  const GameScreen({super.key});

  @override
  State<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> with TickerProviderStateMixin {
  late AnimationController _controller;
  Timer? _gameTimer;
  int _score = 0;

  @override
  void initState() {
    super.initState();
    
    // 初始化动画控制器
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();

    // 启动游戏主循环(每秒更新)
    _gameTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _score++;
        // 模拟游戏失败条件
        if (_score >= 10) {
          _onGameOver();
        }
      });
    });
  }

  // 游戏结束回调
  void _onGameOver() {
    _gameTimer?.cancel();     // ✅ 关键:取消定时器
    _controller.dispose();    // ✅ 关键:释放动画资源
    
    // 跳转到结束页(替换当前页)
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(
        builder: (_) => GameOverScreen(score: _score),
      ),
    );
  }

  @override
  void dispose() {
    // 安全兜底:确保资源被释放
    _gameTimer?.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('游戏中...', style: TextStyle(color: Colors.white, fontSize: 24)),
            const SizedBox(height: 20),
            Text('得分: $_score', style: TextStyle(color: Colors.white, fontSize: 18)),
            const SizedBox(height: 40),
            ElevatedButton(
              onPressed: _onGameOver, // 手动触发结束(测试用)
              child: const Text('强制结束'),
            ),
          ],
        ),
      ),
    );
  }
}

🔥 重点强调

  • _onGameOver()立即取消定时器并 dispose 控制器
  • dispose() 作为最后防线,防止异常路径遗漏清理
  • 使用 TickerProviderStateMixin 提供 vsync,避免动画卡顿

四、GameOverScreen:提供明确出口

结束页的目标是 引导用户下一步操作,通常有两个选项:

  1. 重新开始 → 回到主菜单 或 直接新游戏
  2. 退出 → 返回系统桌面(OpenHarmony 下可调用 SystemNavigator.pop()
dart 复制代码
class GameOverScreen extends StatelessWidget {
  final int score;

  const GameOverScreen({required this.score, super.key});

  void _restartGame(BuildContext context) {
    // 方案A:回到主菜单(推荐,保持流程清晰)
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (_) => const MainMenuScreen()),
    );

    // 方案B:直接启动新游戏(需确保 GameScreen 可重复创建)
    // Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const GameScreen()));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[900],
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('游戏结束!', style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            Text('最终得分: $score', style: TextStyle(color: Colors.white, fontSize: 20)),
            const SizedBox(height: 40),
            ElevatedButton.icon(
              onPressed: () => _restartGame(context),
              icon: const Icon(Icons.replay),
              label: const Text('重新开始'),
              style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
            ),
            const SizedBox(height: 16),
            OutlinedButton(
              onPressed: () {
                // OpenHarmony / Android / iOS 通用退出方式
                SystemNavigator.pop(); // 退出应用(谨慎使用)
                // 或仅返回主菜单:_restartGame(context);
              },
              child: const Text('退出游戏', style: TextStyle(color: Colors.white)),
            ),
          ],
        ),
      ),
    );
  }
}

⚠️ 注意SystemNavigator.pop() 在部分平台可能无效(如 Web),生产环境建议引导至主菜单而非直接退出。


五、MainMenuScreen:轻量入口,无状态负担

主菜单应尽可能简单,避免持有任何游戏相关状态。

dart 复制代码
class MainMenuScreen extends StatelessWidget {
  const MainMenuScreen({super.key});

  void _startGame(BuildContext context) {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (_) => const GameScreen()),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blueGrey[900],
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('球球快跑', style: TextStyle(color: Colors.white, fontSize: 40, fontWeight: FontWeight.bold)),
            const SizedBox(height: 60),
            ElevatedButton.icon(
              onPressed: () => _startGame(context),
              icon: const Icon(Icons.play_arrow),
              label: const Text('开始游戏', style: TextStyle(fontSize: 18)),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
                backgroundColor: Colors.deepOrange,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

✅ 优势:无 StatefulWidget,无资源占用,可无限次进入。


六、完整路由流程图

pushReplacement
pushReplacement
pushReplacement
SystemNavigator.pop
MainMenuScreen
GameScreen
GameOverScreen
退出应用

  • 所有箭头均为单向替换,无返回路径
  • GameScreen 是唯一有状态、需清理的页面

七、运行界面:



八、OpenHarmony 特别提示

  1. 后台清理 :OpenHarmony 应用进入后台时,系统可能回收资源。建议在 AppLifecycleState.paused 时暂停游戏,在 resumed 时恢复或结束。
  2. 退出行为SystemNavigator.pop() 在 OpenHarmony 上表现良好,但需在 AndroidManifest.xml 中配置 finishOnClose(如需要)。
  3. 内存监控 :使用 DevEco Studio 的内存分析工具,验证 GameScreen 销毁后无对象残留。

结语

通过 状态机思维 + pushReplacement 路由 + 严格的 dispose 管理,我们构建了一个健壮、清晰、低内存占用的游戏状态流转系统。

这套模式不仅适用于小球游戏,还可扩展至:

  • 关卡选择 → 游戏 → 暂停菜单 → 设置
  • 登录 → 主界面 → 个人中心 → 退出登录

记住:页面不是堆叠的纸张,而是状态的具象化。状态切换时,旧状态必须彻底死亡。

相关推荐
铅笔侠_小龙虾2 小时前
Flutter 组件层级关系
前端·flutter·servlet
一起养小猫2 小时前
Flutter for OpenHarmony 实战:打地鼠游戏完整开发指南
flutter·游戏·harmonyos
一起养小猫2 小时前
Flutter for OpenHarmony 实战:打地鼠游戏难度设计与平衡性
flutter·游戏·harmonyos
ujainu2 小时前
Flutter + OpenHarmony 实战:构建独立可复用的皮肤选择界面
flutter·游戏·openharmony
多打代码2 小时前
2026.02.05 (贪心)买卖股票2 & 跳跃游戏 1 & 2
游戏
Betelgeuse762 小时前
【Flutter For OpenHarmony】 阶段复盘:从单页Demo到模块化App
flutter·ui·华为·交互·harmonyos
一起养小猫2 小时前
Flutter for OpenHarmony 实战:记忆翻牌游戏完整开发指南
flutter·游戏·harmonyos
lbb 小魔仙3 小时前
【HarmonyOS】DAY13:Flutter电商实战:从零开发注册页面(含密码验证、确认密码完整实现)
flutter·华为·harmonyos
jaysee-sjc3 小时前
【项目二】用GUI编程实现石头迷阵游戏
java·开发语言·算法·游戏