当前位置: 首页 > news >正文

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 等新平台预留接口。

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

  1. 如何使用StatefulWidget管理加载状态与分数数据;
  2. 如何通过shared_preferences安全读写历史最高分(该插件在 OpenHarmony 社区已有兼容层探索);
  3. 如何利用Stack + Positioned构建灵活 UI 布局;
  4. 如何进行错误防御生命周期安全检查,提升代码可移植性。

💡技术栈:Flutter 3.24+、Dart 3.5+、shared_preferences: ^2.3.0
适用场景:小游戏、教育类 App、工具类启动页,OpenHarmony 适配


一、为什么选择 StatefulWidget?

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

classMainMenuScreenextendsStatefulWidget{@override_MainMenuScreenStatecreateState()=>_MainMenuScreenState();}

其对应的State类负责:

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

📌关键原则

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

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


二、使用 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()

@overridevoidinitState(){super.initState();_loadHighScore();}

具体实现如下:

Future<void>_loadHighScore()async{try{finalprefs=awaitSharedPreferences.getInstance();finalscore=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不受子元素尺寸影响,定位自由;
  • 避免使用paddingmargin实现角落布局,更语义化;
  • 此布局在 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

我们只存储一个整数

awaitprefs.setInt('highScore',newScore);

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

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

2. 未来扩展点

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

classSkinScreenextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnScaffold(body:Center(child:Text('皮肤功能开发中...')),);}}

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


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

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

import'package:flutter/material.dart';import'package:shared_preferences/shared_preferences.dart';voidmain()=>runApp(constMyApp());classMyAppextendsStatelessWidget{constMyApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'Flutter + OpenHarmony 主菜单演示',debugShowCheckedModeBanner:false,home:MainMenuScreen(),);}}// ==================== 主菜单页(带历史最高纪录) ====================classMainMenuScreenextendsStatefulWidget{@override_MainMenuScreenStatecreateState()=>_MainMenuScreenState();}class_MainMenuScreenStateextendsState<MainMenuScreen>{int _highScore=0;bool _isLoading=true;@overridevoidinitState(){super.initState();_loadHighScore();}Future<void>_loadHighScore()async{try{finalprefs=awaitSharedPreferences.getInstance();finalscore=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){finalprefs=awaitSharedPreferences.getInstance();awaitprefs.setInt('highScore',newScore);if(mounted){setState((){_highScore=newScore;});}}}@overrideWidgetbuild(BuildContextcontext){returnScaffold(backgroundColor:constColor(0xFF0F0F1A),body:Stack(children:[Center(child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[constText('圆环跳跃',style:TextStyle(fontSize:36,fontWeight:FontWeight.bold,color:Colors.white),),constSizedBox(height:40),Container(padding:constEdgeInsets.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:constTextStyle(color:Colors.amber,fontSize:24,fontWeight:FontWeight.bold,),),),constSizedBox(height:30),// 模拟“开始游戏”后获得新分数Wrap(spacing:15,runSpacing:10,children:[ElevatedButton(onPressed:()=>_simulateNewGame(15),style:ElevatedButton.styleFrom(backgroundColor:Colors.green),child:constText('模拟得15分'),),ElevatedButton(onPressed:()=>_simulateNewGame(25),style:ElevatedButton.styleFrom(backgroundColor:Colors.blue),child:constText('模拟得25分'),),],),constSizedBox(height:20),ElevatedButton(onPressed:()=>Navigator.pushReplacement(context,MaterialPageRoute(builder:(_)=>MainMenuScreen()),// 刷新页面),style:ElevatedButton.styleFrom(padding:constEdgeInsets.symmetric(horizontal:50,vertical:18),textStyle:constTextStyle(fontSize:22),backgroundColor:constColor(0xFF4E54C8),foregroundColor:Colors.cyan,),child:constText('刷新页面'),),],),),Positioned(top:40,left:30,child:ElevatedButton.icon(onPressed:(){// 此处可跳转到皮肤页ScaffoldMessenger.of(context).showSnackBar(constSnackBar(content:Text('皮肤功能即将上线!')),);},icon:constIcon(Icons.palette,size:20,color:Colors.white),label:constText('皮肤',style:TextStyle(fontSize:16,color:Colors.white)),style:ElevatedButton.styleFrom(backgroundColor:Colors.deepPurple.shade700,shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(20)),),),),],),);}}

运行界面:

🔧使用说明

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

结语

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

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

http://www.jsqmd.com/news/330382/

相关文章:

  • 12306抢票软件(自动抢票、定时抢票、自动支付、自动候补)
  • 互联网大厂Java面试实战:从Spring Boot到Kafka的技术与业务场景解析
  • 容器编排 - 了解K8s(pod, deployment,service,lable等概念)
  • 容器编排 - K8s - 配置文件参数说明和基础命令
  • 用于管理和协调多个进程:用于Docker容器的Supervisor配置文件
  • 【动手学深度学习】第三课 数据预处理
  • 揭秘AI教材编写秘籍,使用AI写教材,有效控制查重率!
  • AI应用测试用例之千问工具(4)
  • AI写论文不用愁!4款AI论文写作利器,一键解决写作难题!
  • 2026年1月31日-2天10万Star!GitHub史上最快开源项目OpenClaw,手把手教你免费实现部署私人AI助手
  • VASP+PHONOPY+pypolymlpj计算不同温度下声子谱,附批处理脚本
  • 图像算法优化常用方式-vivado hls设计
  • 深入解析:RIB表与FIB表的区别
  • 实用指南:让 ETL 更懂语义:DataWorks 支持数据集成 AI 辅助处理能力
  • 电子学会青少年软件编程(C语言)等级考试试卷(一级)2025年12月
  • [运营实战] 节日大促图片来不及做?浅析如何用 AI 批量汉化与修改“季节性”卖点图,灵活承接旺季流量
  • vue 插槽详解
  • 语法:一文搞懂“双宾语”与“宾语补足语”
  • Lazarus的lazlogger单元使用
  • ANTLR4:解析器生成工具的强大力量
  • metahuman 购买安装记录
  • 数据搬运工-DMA(上)
  • AI技术点总结(2)
  • 深度解析Android系统开发工程师岗位:技术体系与实战指南(含面试题库)
  • VisionPro视觉检测软件之打包成安装包
  • 基于Simulink的A*算法自动驾驶路径规划仿真建模示例
  • 开发跨部门沟通话术生成器,按场景(需求对接/问题协调/资源申请),生成礼貌高效话术,兼顾立场和沟通效率,减少跨部门沟通矛盾。
  • 基于Simulink的超声波传感器障碍物检测仿真建模示例
  • 【游戏推荐】ServiceIT:你可以做IT (ServiceIT You can do IT)免安装中文版
  • 2002-2025年各省、地级市农业新质生产力数据