欢迎光临
我们一直在努力

Flutter + OpenHarmony 游戏开发进阶:主菜单架构与历史最高分持久化

在这里插入图片描述 个人主页:ujainu

文章目录

    • 引言
    • 一、为什么选择 StatefulWidget?
    • 二、使用 shared_preferences 持久化历史最高分
      • 1. 什么是 shared_preferences?
      • 2. 初始化与读取
        • 代码详解:
    • 三、UI 布局:Stack + Positioned 的灵活组合
      • 1. 主体内容:Column 居中
      • 2. 角落按钮:Positioned 精确定位
    • 四、错误处理与用户体验优化
      • 1. 加载状态反馈
      • 2. 异常静默处理
      • 3. 生命周期安全:mounted 检查
    • 五、性能与扩展性考虑
      • 1. 数据隔离:仅存 highScore
      • 2. 未来扩展点
    • 六、完整可运行代码(贴合主题)
    • 结语

引言

随着 OpenHarmony 生态的快速演进,越来越多的开发者开始关注如何将现有 Flutter 应用高效迁移或适配到这一开源分布式操作系统上。尽管 OpenHarmony 目前对 Flutter 的原生支持仍在完善中(可通过 ArkTS 桥接或社区方案运行),但良好的代码结构、轻量级数据存储、健壮的状态管理,正是确保应用未来平滑迁移的关键前提。

在游戏开发中,主菜单页不仅是用户进入游戏的第一印象,更是承载核心元数据(如历史最高分)的关键入口。一个优秀的主菜单应具备:

  • 快速响应:避免卡顿或白屏;
  • 数据可靠:即使应用被杀,历史记录仍能保留;
  • 视觉清晰:突出“开始游戏”与“历史成绩”;
  • 健壮性:网络异常、存储失败等边界情况需优雅处理;
  • 跨平台友好:为未来适配 OpenHarmony 等新平台预留接口。

本文将聚焦于 主菜单页面的完整实现,重点讲解:

  • 如何使用 StatefulWidget 管理加载状态与分数数据;
  • 如何通过 shared_preferences 安全读写历史最高分(该插件在 OpenHarmony 社区已有兼容层探索);
  • 如何利用 Stack + Positioned 构建灵活 UI 布局;
  • 如何进行错误防御与生命周期安全检查,提升代码可移植性。
  • 💡 技术栈:Flutter 3.24+、Dart 3.5+、shared_preferences: ^2.3.0 ✅ 适用场景:小游戏、教育类 App、工具类启动页,OpenHarmony 适配


    一、为什么选择 StatefulWidget?

    主菜单看似静态,实则包含动态数据(历史最高分)和加载状态(“加载中…”)。因此必须使用 StatefulWidget:

    class MainMenuScreen extends StatefulWidget {

    _MainMenuScreenState createState() => _MainMenuScreenState();
    }

    其对应的 State 类负责:

    • 声明状态变量(_highScore, _isLoading);
    • 在 initState 中触发异步加载;
    • 通过 setState 更新 UI。

    📌 关键原则:

    • 状态最小化:只存必要数据(int _highScore),不存复杂对象;
    • 初始化分离:数据加载逻辑放在 initState,而非 build。

    这种设计不仅符合 Flutter 最佳实践,也为未来在 OpenHarmony 上通过 Platform Channel 或 FFI 调用本地存储能力提供了清晰的数据边界。


    二、使用 shared_preferences 持久化历史最高分

    1. 什么是 shared_preferences?

    shared_preferences 是 Flutter 官方提供的轻量级键值对存储方案,底层对应:

    • Android:SharedPreferences
    • iOS:NSUserDefaults
    • Web:localStorage

    在 OpenHarmony 社区,已有开发者基于 Preferences Kit(OpenHarmony 提供的轻量级数据存储能力)封装了兼容 shared_preferences 接口的桥接层。这意味着,只要我们遵循标准 API 调用,未来迁移到 OpenHarmony 将只需替换依赖,无需重写业务逻辑。

    ⚠️ 不适用场景:大量结构化数据(应使用 SQLite 或 Hive)。

    2. 初始化与读取

    我们在 initState 中调用 _loadHighScore():


    void initState() {
    super.initState();
    _loadHighScore();
    }

    具体实现如下:

    Future<void> _loadHighScore() async {
    try {
    final prefs = await SharedPreferences.getInstance();
    final score = prefs.getInt('highScore') ?? 0;
    if (mounted) {
    setState(() {
    _highScore = score;
    _isLoading = false;
    });
    }
    } catch (e) {
    // 加载失败时,隐藏加载状态,但不崩溃
    if (mounted) setState(() => _isLoading = false);
    }
    }

    代码详解:
    行说明
    prefs.getInt('highScore') 尝试读取整数类型键值
    ?? 0 若首次启动(无数据),默认返回 0
    if (mounted) 关键安全检查!防止异步回调时组件已销毁
    catch (e) 捕获存储异常(如权限问题、磁盘满)

    ✅ 最佳实践:

    • 键名规范:使用小驼峰 highScore,避免特殊字符;
    • 默认值明确:?? 0 比 ! 更安全;
    • 为 OpenHarmony 预留抽象层:未来可将 SharedPreferences.getInstance() 替换为统一的 StorageService.get()。

    三、UI 布局:Stack + Positioned 的灵活组合

    主菜单需要同时满足:

    • 居中内容(标题、最高分、开始按钮);
    • 角落控件(右上角“皮肤”按钮)。

    此时 Stack 是最佳选择:

    body: Stack(
    children: [
    Center( /* 主体内容 */ ),
    Positioned(top: 40, left: 30, child: /* 皮肤按钮 */),
    ],
    )

    1. 主体内容:Column 居中

    Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
    Text('圆环跳跃', style: ...),
    Container( /* 最高分展示 */ ),
    ElevatedButton( /* 开始游戏 */ ),
    ],
    ),
    )

    • mainAxisAlignment: MainAxisAlignment.center:垂直居中;
    • 使用 Container + BoxDecoration 自定义最高分样式,比纯 Text 更醒目。

    2. 角落按钮:Positioned 精确定位

    Positioned(
    top: 40,
    left: 30,
    child: ElevatedButton.icon(
    onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => SkinScreen())),
    icon: Icon(Icons.palette),
    label: Text('皮肤'),
    ),
    )

    • top: 40, left: 30:距离顶部 40px,左侧 30px;
    • 使用 ElevatedButton.icon 节省空间,符合 Material Design。

    💡 布局优势:

    • Stack 不受子元素尺寸影响,定位自由;
    • 避免使用 padding 或 margin 实现角落布局,更语义化;
    • 此布局在 OpenHarmony 的不同设备形态(手机、平板、智慧屏)上也能通过响应式调整良好适配。

    四、错误处理与用户体验优化

    1. 加载状态反馈

    在数据未加载完成前,显示“加载中…”:

    Text(
    _isLoading ? '加载中…' : '历史最高:$_highScore',
    style: TextStyle(color: Colors.amber, fontSize: 24),
    )

    • 避免用户看到“0分”误以为是真实成绩;
    • 使用琥珀色强调重要信息。

    2. 异常静默处理

    即使 shared_preferences 读取失败(如模拟器沙盒限制),我们也不抛出错误,而是:

    • 关闭 _isLoading 状态;
    • 显示默认值 0。

    这保证了应用可用性,符合“Fail Gracefully”原则,也是 OpenHarmony 分布式应用所倡导的弹性体验。

    3. 生命周期安全:mounted 检查

    这是 Flutter 异步编程的黄金法则:

    if (mounted) {
    setState(() { ... });
    }

    ❌ 危险写法:

    _loadHighScore().then((_) {
    setState(() { ... }); // 可能 crash!
    });

    因为当页面快速关闭(如用户点击返回),setState 会在已销毁的 State 上调用,导致:

    setState() called after dispose()

    解决方案:始终在 setState 前加 mounted 判断。这一习惯在多端适配(包括 OpenHarmony)时尤为重要,因不同平台生命周期行为可能存在差异。


    五、性能与扩展性考虑

    1. 数据隔离:仅存 highScore

    我们只存储一个整数:

    await prefs.setInt('highScore', newScore);

    而非存储整个游戏状态(如轨道列表、球位置)。原因:

    • 存储效率高:整数序列化快,占用空间小;
    • 兼容性强:未来版本更新不会因结构变化导致解析失败;
    • 安全性好:避免敏感数据泄露;
    • 便于 OpenHarmony 迁移:Preferences Kit 原生支持基本类型,无需复杂序列化。

    2. 未来扩展点

    当前“皮肤”按钮跳转到占位页:

    class SkinScreen extends StatelessWidget {

    Widget build(BuildContext context) {
    return Scaffold(
    body: Center(child: Text('皮肤功能开发中…')),
    );
    }
    }

    这种预留框架设计便于后续迭代,且不影响主流程。未来若 OpenHarmony 提供主题服务,可无缝接入。


    六、完整可运行代码(贴合主题)

    将以下代码保存为 lib/main_menu_demo.dart,即可独立运行主菜单模块:

    import 'package:flutter/material.dart';
    import 'package:shared_preferences/shared_preferences.dart';

    void main() => runApp(const MyApp());

    class MyApp extends StatelessWidget {
    const MyApp({super.key});


    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter + OpenHarmony 主菜单演示',
    debugShowCheckedModeBanner: false,
    home: MainMenuScreen(),
    );
    }
    }

    // ==================== 主菜单页(带历史最高纪录) ====================
    class MainMenuScreen extends StatefulWidget {

    _MainMenuScreenState createState() => _MainMenuScreenState();
    }

    class _MainMenuScreenState extends State<MainMenuScreen> {
    int _highScore = 0;
    bool _isLoading = true;


    void initState() {
    super.initState();
    _loadHighScore();
    }

    Future<void> _loadHighScore() async {
    try {
    final prefs = await SharedPreferences.getInstance();
    final score = prefs.getInt('highScore') ?? 0;
    if (mounted) {
    setState(() {
    _highScore = score;
    _isLoading = false;
    });
    }
    } catch (e) {
    // 即使加载失败,也确保 UI 可交互
    if (mounted) setState(() => _isLoading = false);
    }
    }

    Future<void> _simulateNewGame(int newScore) async {
    // 模拟游戏结束后保存新分数
    if (newScore > _highScore) {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt('highScore', newScore);
    if (mounted) {
    setState(() {
    _highScore = newScore;
    });
    }
    }
    }


    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: const Color(0xFF0F0F1A),
    body: Stack(
    children: [
    Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
    const Text(
    '圆环跳跃',
    style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white),
    ),
    const SizedBox(height: 40),
    Container(
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
    decoration: BoxDecoration(
    color: Colors.amber.withOpacity(0.15),
    border: Border.all(color: Colors.amber, width: 2),
    borderRadius: BorderRadius.circular(16),
    ),
    child: Text(
    _isLoading ? '加载中…' : '历史最高:$_highScore',
    style: const TextStyle(
    color: Colors.amber,
    fontSize: 24,
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    const SizedBox(height: 30),
    // 模拟“开始游戏”后获得新分数
    Wrap(
    spacing: 15,
    runSpacing: 10,
    children: [
    ElevatedButton(
    onPressed: () => _simulateNewGame(15),
    style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
    child: const Text('模拟得15分'),
    ),
    ElevatedButton(
    onPressed: () => _simulateNewGame(25),
    style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
    child: const Text('模拟得25分'),
    ),
    ],
    ),
    const SizedBox(height: 20),
    ElevatedButton(
    onPressed: () => Navigator.pushReplacement(
    context,
    MaterialPageRoute(builder: (_) => MainMenuScreen()), // 刷新页面
    ),
    style: ElevatedButton.styleFrom(
    padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 18),
    textStyle: const TextStyle(fontSize: 22),
    backgroundColor: const Color(0xFF4E54C8),
    foregroundColor: Colors.cyan,
    ),
    child: const Text('刷新页面'),
    ),
    ],
    ),
    ),
    Positioned(
    top: 40,
    left: 30,
    child: ElevatedButton.icon(
    onPressed: () {
    // 此处可跳转到皮肤页
    ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('皮肤功能即将上线!')),
    );
    },
    icon: const Icon(Icons.palette, size: 20, color: Colors.white),
    label: const Text('皮肤', style: TextStyle(fontSize: 16, color: Colors.white)),
    style: ElevatedButton.styleFrom(
    backgroundColor: Colors.deepPurple.shade700,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
    ),
    ),
    ),
    ],
    ),
    );
    }
    }

    运行界面: 在这里插入图片描述 在这里插入图片描述

    🔧 使用说明:

  • 添加依赖:shared_preferences: ^2.3.0;
  • 点击“模拟得XX分”可测试分数保存;
  • 点击“刷新页面”验证数据持久化;
  • 面向 OpenHarmony 适配建议:未来可将 shared_preferences 替换为社区提供的 OpenHarmony 兼容包,业务逻辑无需改动。

  • 结语

    主菜单虽小,却是用户体验的第一道关卡,更是跨平台迁移的基石。通过合理使用 StatefulWidget、安全调用 shared_preferences、精心设计 Stack 布局,我们不仅构建了一个健壮、美观、可扩展的主菜单系统,更为未来适配 OpenHarmony 等新兴生态打下了坚实基础。

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

    赞(0)
    未经允许不得转载:171主机测评 » Flutter + OpenHarmony 游戏开发进阶:主菜单架构与历史最高分持久化
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址