
个人主页:
文章目录
-
- 引言
- [一、项目初始化:强制横屏与 MyApp 入口](#一、项目初始化:强制横屏与 MyApp 入口)
-
- [1. 为什么游戏要强制横屏?](#1. 为什么游戏要强制横屏?)
-
- [实现方式:在 `main()` 中设置系统 UI](#实现方式:在
main()中设置系统 UI)
- [实现方式:在 `main()` 中设置系统 UI](#实现方式:在
- [2. MyApp:统一主题与路由入口](#2. MyApp:统一主题与路由入口)
- [二、主菜单页面:MainMenuScreen 的状态管理与 UI 构建](#二、主菜单页面:MainMenuScreen 的状态管理与 UI 构建)
-
- [1. 核心需求分析](#1. 核心需求分析)
- [2. StatefulWidget 管理异步状态](#2. StatefulWidget 管理异步状态)
-
- [🔍 关键设计点:](#🔍 关键设计点:)
- [三、数据持久化:使用 shared_preferences 存储最高分](#三、数据持久化:使用 shared_preferences 存储最高分)
-
- [1. 添加依赖](#1. 添加依赖)
- [2. 读写最佳实践](#2. 读写最佳实践)
-
- [读取(已展示在 `_loadHighScore`):](#读取(已展示在
_loadHighScore):) - 写入(通常在游戏结束时调用):
- [读取(已展示在 `_loadHighScore`):](#读取(已展示在
- [3. 在 OpenHarmony 上的兼容性](#3. 在 OpenHarmony 上的兼容性)
- [四、页面导航:为何使用 pushReplacement?](#四、页面导航:为何使用 pushReplacement?)
-
- [1. 场景分析](#1. 场景分析)
- [2. 导航策略](#2. 导航策略)
- [五、占位页面:SkinScreen 的扩展设计](#五、占位页面:SkinScreen 的扩展设计)
- 六、代码示例:
- 六、总结与下篇预告
引言
在移动应用生态日益多元的今天,OpenHarmony 作为开源的分布式操作系统,正迅速构建起属于自己的应用生态。而 Flutter 凭借其高性能渲染、跨平台一致性以及丰富的 UI 能力,成为许多开发者构建 OpenHarmony 应用的首选框架。尤其对于游戏类轻应用,Flutter 的自绘引擎(Skia)能轻松实现流畅动画与精美界面,同时通过 shared_preferences 等插件实现本地数据持久化。
本文是《Flutter + OpenHarmony 小游戏实战》系列的第一篇,我们将从 零开始搭建一个完整的小游戏项目骨架,重点实现:
- ✅ 横屏强制锁定(适配游戏场景);
- ✅ 主菜单页面(MainMenuScreen):含动态加载、最高分显示;
- ✅ 皮肤设置页(SkinScreen):预留扩展入口;
- ✅ 最高分持久化 :使用
shared_preferences存储与读取; - ✅ 页面导航优化 :使用
Navigator.pushReplacement避免返回栈堆积。
💡 目标设备:本文代码可直接运行于支持 Flutter 的 OpenHarmony 设备(如 HarmonyOS NEXT 测试机或社区移植版),亦兼容 Android/iOS。
一、项目初始化:强制横屏与 MyApp 入口
1. 为什么游戏要强制横屏?
绝大多数休闲/动作类小游戏采用 横屏布局,以提供更宽广的操作视野和沉浸式体验。在 OpenHarmony 设备上,我们需在应用启动时即锁定方向。
实现方式:在 main() 中设置系统 UI
dart
import 'package:flutter/services.dart';
void main() {
// 锁定为横屏(landscape)
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 禁用状态栏(可选,提升沉浸感)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
runApp(const MyApp());
}
📌 说明:
setPreferredOrientations告诉系统仅允许横屏;SystemUiMode.immersiveSticky隐藏状态栏与导航栏,适用于全屏游戏;- 此设置对 OpenHarmony 和 Android 均有效(iOS 需额外配置 Info.plist)。
2. MyApp:统一主题与路由入口
dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flappy Bird Clone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark, // 游戏常用深色背景
scaffoldBackgroundColor: Colors.black,
textTheme: const TextTheme(
headlineMedium: TextStyle(color: Colors.white, fontSize: 48),
bodyMedium: TextStyle(color: Colors.white70, fontSize: 24),
),
),
home: MainMenuScreen(),
);
}
}
- 深色主题:减少 OLED 屏幕功耗,提升视觉对比度;
- 统一字体颜色 :避免各页面重复定义
TextStyle; - 入口设为
MainMenuScreen:用户打开即见主菜单。
二、主菜单页面:MainMenuScreen 的状态管理与 UI 构建
1. 核心需求分析
主菜单需展示:
- 游戏标题(大字居中);
- "开始游戏"按钮;
- "皮肤设置"按钮;
- 历史最高分(从本地读取);
- 加载态处理:首次进入时异步读取高分。
2. StatefulWidget 管理异步状态
dart
class MainMenuScreen extends StatefulWidget {
@override
State<MainMenuScreen> createState() => _MainMenuScreenState();
}
class _MainMenuScreenState extends State<MainMenuScreen> {
int? _highScore; // 可空,表示"正在加载"
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadHighScore();
}
Future<void> _loadHighScore() async {
final prefs = await SharedPreferences.getInstance();
final score = prefs.getInt('high_score') ?? 0;
if (mounted) {
setState(() {
_highScore = score;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 背景(可替换为图片)
Container(color: Colors.black),
// 内容层
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('FLAPPY BIRD', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
const SizedBox(height: 40),
// 最高分显示
if (_isLoading)
const CircularProgressIndicator(color: Colors.white)
else
Text('最高分: $_highScore', style: const TextStyle(fontSize: 24)),
const SizedBox(height: 60),
// 按钮区域
ElevatedButton(
onPressed: () => _startGame(),
child: const Text('开始游戏', style: TextStyle(fontSize: 20)),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _goToSkin(),
child: const Text('皮肤设置', style: TextStyle(fontSize: 20)),
),
],
),
),
],
),
);
}
void _startGame() {
// TODO: 跳转到游戏页面
}
void _goToSkin() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SkinScreen()),
);
}
}
🔍 关键设计点:
| 特性 | 实现方式 | 优势 |
|---|---|---|
| 加载态 | _isLoading + CircularProgressIndicator |
用户感知"正在准备",避免空白 |
| 安全更新 | if (mounted) 检查 |
防止页面销毁后调用 setState |
| UI 布局 | Stack + Positioned.fill + Column |
背景与内容分离,弹性布局 |
| 深色适配 | 白色文字 + 黑色背景 | 符合游戏视觉规范 |
💡 为什么用
Stack?
- 背景可轻松替换为
Image或AnimatedBackground;- 内容层居中不受背景影响;
- 未来可叠加粒子特效(如飘落的羽毛)。
三、数据持久化:使用 shared_preferences 存储最高分
1. 添加依赖
在 pubspec.yaml 中添加:
yaml
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.0
2. 读写最佳实践
读取(已展示在 _loadHighScore):
dart
final prefs = await SharedPreferences.getInstance();
final score = prefs.getInt('high_score') ?? 0;
写入(通常在游戏结束时调用):
dart
Future<void> saveHighScore(int newScore) async {
final prefs = await SharedPreferences.getInstance();
final current = prefs.getInt('high_score') ?? 0;
if (newScore > current) {
await prefs.setInt('high_score', newScore);
}
}
✅ 最佳实践总结:
- 键名规范 :使用
'high_score'而非'score',语义清晰;- 空值处理 :
?? 0避免首次启动崩溃;- 只存最高分:不存所有历史记录,节省空间;
- 异步安全 :所有操作均为
Future,不阻塞 UI。
3. 在 OpenHarmony 上的兼容性
shared_preferences 在 OpenHarmony 上通过 Dart FFI 调用原生存储 API 实现。社区已有适配版本(如 flutter_ohos 插件包),确保数据持久化行为与 Android/iOS 一致。
⚠️ 注意:若使用纯 OpenHarmony SDK,需确认插件是否支持。建议优先选用官方或华为认证的 Flutter for OpenHarmony 运行时。
四、页面导航:为何使用 pushReplacement?
1. 场景分析
- 用户从 主菜单 → 皮肤设置:应能返回主菜单;
- 用户从 主菜单 → 游戏页面 :不应能返回主菜单(否则会重置游戏);
- 游戏结束后,应 返回主菜单,而非回到"上一局"。
2. 导航策略
| 跳转路径 | 使用方法 | 原因 |
|---|---|---|
| 主菜单 → 皮肤页 | Navigator.push() |
允许返回 |
| 主菜单 → 游戏页 | Navigator.pushReplacement() |
清空返回栈,防止误返回中断游戏 |
| 游戏结束 → 主菜单 | Navigator.pushReplacement() |
直接回到起点,体验干净 |
示例:开始游戏
dart
void _startGame() {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => GameScreen()),
);
}
🧠 用户体验思考 :
游戏是"一次性会话",用户结束即回到主菜单重新开始。若保留返回栈,用户可能误触返回导致成绩丢失,这是糟糕的设计。
五、占位页面:SkinScreen 的扩展设计
dart
class SkinScreen extends StatelessWidget {
const SkinScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('皮肤设置')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text('功能开发中...', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
Text('敬请期待!', style: TextStyle(color: Colors.grey)),
],
),
),
);
}
}
- 独立页面:便于未来接入皮肤选择逻辑;
- AppBar 返回:自动带有返回按钮,符合 Material 规范;
- 占位文案:明确告知用户功能未上线,提升体验。
六、代码示例:
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
// 锁定横屏 + 沉浸式模式
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flappy Bird Clone',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.black,
textTheme: const TextTheme(
headlineMedium: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold),
bodyMedium: TextStyle(color: Colors.white70, fontSize: 24),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
textStyle: const TextStyle(fontSize: 20),
),
),
),
home: MainMenuScreen(),
);
}
}
class MainMenuScreen extends StatefulWidget {
@override
State<MainMenuScreen> createState() => _MainMenuScreenState();
}
class _MainMenuScreenState extends State<MainMenuScreen> {
int? _highScore;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadHighScore();
}
Future<void> _loadHighScore() async {
final prefs = await SharedPreferences.getInstance();
final score = prefs.getInt('high_score') ?? 0;
if (mounted) {
setState(() {
_highScore = score;
_isLoading = false;
});
}
}
void _startGame() {
// 模拟:游戏结束后会保存分数(这里先保存一个测试分数)
// 实际项目中,此处应跳转到 GameScreen()
_simulateGameEnd();
// 跳转到"游戏页面"(用 MainMenuScreen 自身模拟,实际替换为 GameScreen)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const GameResultScreen()),
);
}
void _simulateGameEnd() async {
// 模拟一局游戏结束,随机生成分数并尝试更新最高分
final randomScore = 50 + (DateTime.now().millisecondsSinceEpoch % 100);
final prefs = await SharedPreferences.getInstance();
final currentHigh = prefs.getInt('high_score') ?? 0;
if (randomScore > currentHigh) {
await prefs.setInt('high_score', randomScore);
}
}
void _goToSkin() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SkinScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 背景
Container(
color: Colors.black,
),
// 内容
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('FLAPPY BIRD'),
const SizedBox(height: 40),
if (_isLoading)
const CircularProgressIndicator(color: Colors.white)
else
Text('最高分: $_highScore', style: const TextStyle(fontSize: 24)),
const SizedBox(height: 60),
ElevatedButton(
onPressed: _startGame,
child: const Text('开始游戏'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _goToSkin,
child: const Text('皮肤设置'),
),
],
),
),
],
),
);
}
}
// 模拟游戏结果页(实际项目中替换为真实游戏)
class GameResultScreen extends StatelessWidget {
const GameResultScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('游戏结束!', style: TextStyle(fontSize: 36)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => MainMenuScreen()),
);
},
child: const Text('返回主菜单'),
),
],
),
),
);
}
}
// 皮肤设置页(占位)
class SkinScreen extends StatelessWidget {
const SkinScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('皮肤设置')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.palette, size: 64, color: Colors.blue),
SizedBox(height: 20),
Text('皮肤功能开发中...', style: TextStyle(fontSize: 24)),
SizedBox(height: 10),
Text('敬请期待!', style: TextStyle(color: Colors.grey)),
],
),
),
);
}
}
运行界面:

六、总结与下篇预告
本文完成了小游戏项目的 基础架构搭建,实现了:
- ✅ 横屏锁定与沉浸式 UI;
- ✅ 主菜单动态加载最高分;
- ✅ 安全的数据持久化方案;
- ✅ 合理的页面导航策略。
这些看似简单的功能,实则蕴含了 状态管理、异步处理、用户体验、跨平台兼容 等核心工程思想。尤其在 OpenHarmony 生态中,如何让 Flutter 应用既保持开发效率,又符合系统规范,是我们持续探索的方向。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net