鸿蒙原生应用实战(一):项目创建与首页开发 --- 从零搭建数独游戏
前言
随着鸿蒙生态的快速发展,越来越多的开发者开始投身鸿蒙原生应用开发。本系列文章将以一款经典数独游戏为实战项目,从零开始带你体验鸿蒙原生应用(HarmonyOS Next + ArkTS)的完整开发流程。
本篇是第一篇,我们将完成以下内容:
- 使用 DevEco Studio 创建 Stage 模型项目
- 理解 ArkTS 组件化开发模式
- 开发游戏首页,实现难度选择与页面路由
- 掌握资源文件(string/color/float)的最佳实践
一、项目创建与环境配置
1.1 创建 Stage 模型项目
打开 DevEco Studio,选择 File → New → Create Project ,模板选择 Empty Ability(Stage模型)。
关键配置项:
| 配置项 | 值 |
|---|---|
| Project Name | MyApplication |
| Bundle Name | com.sudoku.app |
| Compile SDK | API 9+ |
| Model | Stage |
| Language | ArkTS |
为什么选择 Stage 模型?
Stage 模型是鸿蒙从 API 9 开始主推的 Ability 框架。相比于旧版的 FA(Feature Ability)模型,Stage 模型的优势非常明显:
- 组件化设计 :Ability 和 UI 分离,每个 Ability 有独立的
module.json5配置 - 进程管理:支持多实例和进程级隔离
- 后台任务:更规范的后台任务管理机制
- 生命周期清晰 :
onCreate→onWindowStageCreate→onForeground→onBackground→onDestroy
1.2 项目结构总览
创建完成后,项目结构如下:
MyApplication/
├── AppScope/ # 全局配置
│ ├── app.json5 # 应用级配置(bundleName, version等)
│ └── resources/
├── entry/ # 应用入口模块
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/ # Ability 生命周期
│ │ │ └── pages/ # 页面文件
│ │ ├── module.json5 # 模块配置
│ │ └── resources/ # 资源文件
│ └── build-profile.json5 # 模块构建配置
├── hvigor/ # 构建工具配置
└── oh-package.json5 # 包依赖
1.3 AppScope 全局配置
AppScope/app.json5 用来定义应用的全局属性:
json5
{
"app": {
"bundleName": "com.sudoku.app",
"vendor": "atomcode",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
注意:app_name 只需要在 AppScope/resources/base/element/string.json 中定义一次,不能 在 entry 模块中重复定义,否则会编译冲突。
二、理解 EntryAbility --- 应用入口
EntryAbility.ets 是整个应用的入口能力文件。它继承自 UIAbility,负责管理应用的生命周期:
typescript
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 设置颜色模式
this.context.getApplicationContext().setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// 加载首页
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load. Cause: %{public}s', JSON.stringify(err));
}
});
}
onForeground(): void { /* 应用切到前台 */ }
onBackground(): void { /* 应用切到后台 */ }
onDestroy(): void { /* 应用销毁 */ }
}
关键点 :通过 windowStage.loadContent('pages/Index', callback) 加载首页,页面路径必须与 main_pages.json 中注册的一致。
三、注册页面路由
所有页面都需要在 entry/src/main/resources/base/profile/main_pages.json 中注册:
json
{
"src": [
"pages/Index",
"pages/GamePage",
"pages/LeaderboardPage",
"pages/StatsPage",
"pages/SettingsPage",
"pages/TutorialPage",
"pages/AchievementsPage",
"pages/CustomThemePage"
]
}
本项目共8个页面,后续每开发一个页面,都要先在这里注册。
四、资源文件的最佳实践
鸿蒙推荐使用 $r 语法引用资源,而不是硬编码字符串和颜色。这样做的好处:
- 多语言适配:只需替换资源文件
- 主题切换:运行时改变资源引用
- 视觉规范统一:所有尺寸/颜色集中在资源文件中
4.1 字符串资源 string.json
json
{
"string": [
{ "name": "title_home", "value": "数独" },
{ "name": "difficulty_easy", "value": "简单" },
{ "name": "difficulty_medium", "value": "中等" },
{ "name": "difficulty_hard", "value": "困难" },
{ "name": "daily_challenge", "value": "每日挑战" },
{ "name": "title_leaderboard", "value": "排行榜" },
{ "name": "title_stats", "value": "统计" },
{ "name": "title_settings", "value": "设置" }
]
}
4.2 颜色资源 color.json
json
{
"color": [
{ "name": "primary", "value": "#FF5C6BC0" },
{ "name": "background", "value": "#FFF5F5F5" },
{ "name": "card_bg", "value": "#FFFFFF" },
{ "name": "text_primary", "value": "#FF333333" },
{ "name": "text_secondary", "value": "#FF666666" },
{ "name": "text_hint", "value": "#FF999999" },
{ "name": "cell_given", "value": "#FF333333" },
{ "name": "cell_user", "value": "#FF1565C0" },
{ "name": "cell_error", "value": "#FFF44336" },
{ "name": "cell_selected", "value": "#FFBBDEFB" }
]
}
4.3 字号资源 float.json
json
{
"float": [
{ "name": "page_title_font_size", "value": "22fp" },
{ "name": "body_font_size", "value": "16fp" },
{ "name": "small_font_size", "value": "13fp" },
{ "name": "padding_medium", "value": "16vp" },
{ "name": "card_corner_radius", "value": "12vp" }
]
}
使用方式:$r('app.color.primary')、$r('app.string.title_home')、$r('app.float.body_font_size')
注意 :
fp是字体像素单位,会根据系统字体缩放;vp是虚拟像素单位,用于布局尺寸,保证不同屏幕密度下视觉一致。
五、首页开发 --- Index.ets
首页是用户打开应用后看到的第一个界面。我们的数独首页包含:
- 标题区域(emoji + 应用名 + 副标题)
- 四个难度选择按钮(简单/中等/困难/每日挑战)
- 底部导航链接(排行榜/统计/设置)
5.1 完整的首页代码
typescript
import router from '@ohos.router';
interface RouteOpt {
url: string;
params?: Object;
}
interface DiffParams {
difficulty: string;
}
@Entry
@Component
struct Index {
build() {
Column() {
// === 标题区域 ===
Column() {
Text('🎲')
.fontSize(64)
Text($r('app.string.title_home'))
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.margin({ top: 8 })
Text('经典数独 挑战大脑')
.fontSize($r('app.float.body_font_size'))
.fontColor($r('app.color.text_hint'))
.margin({ top: 4 })
}
.width('100%')
.margin({ top: 80 })
.alignItems(HorizontalAlign.Center)
// === 难度选择按钮 ===
Column() {
// 简单
Button($r('app.string.difficulty_easy'))
.width('80%').height(52)
.backgroundColor($r('app.color.primary'))
.borderRadius(26)
.fontColor(Color.White).fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 16 })
.onClick(() => {
router.pushUrl({ url: 'pages/GamePage', params: { difficulty: 'easy' } })
})
// 中等
Button($r('app.string.difficulty_medium'))
.width('80%').height(52)
.backgroundColor($r('app.color.primary'))
.borderRadius(26)
.fontColor(Color.White).fontSize(18)
.margin({ bottom: 16 })
.onClick(() => {
router.pushUrl({ url: 'pages/GamePage', params: { difficulty: 'medium' } })
})
// 困难
Button($r('app.string.difficulty_hard'))
.width('80%').height(52)
.backgroundColor($r('app.color.primary'))
.borderRadius(26)
.fontColor(Color.White).fontSize(18)
.margin({ bottom: 24 })
.onClick(() => {
router.pushUrl({ url: 'pages/GamePage', params: { difficulty: 'hard' } })
})
// 每日挑战(描边按钮样式)
Button($r('app.string.daily_challenge'))
.width('80%').height(52)
.backgroundColor(Color.White)
.borderRadius(26)
.fontColor($r('app.color.primary')).fontSize(18)
.border({ width: 2, color: $r('app.color.primary') })
.onClick(() => {
router.pushUrl({ url: 'pages/GamePage', params: { difficulty: 'daily' } })
})
}
.width('100%')
.margin({ top: 60 })
.alignItems(HorizontalAlign.Center)
Blank()
// === 底部导航 ===
Row() {
Text('🏆 ')
Text($r('app.string.title_leaderboard'))
}
.margin({ bottom: 12 })
.onClick(() => { router.pushUrl({ url: 'pages/LeaderboardPage' }) })
Row() {
Text('📊 ')
Text($r('app.string.title_stats'))
}
.margin({ bottom: 12 })
.onClick(() => { router.pushUrl({ url: 'pages/StatsPage' }) })
Row() {
Text('⚙️ ')
Text($r('app.string.title_settings'))
}
.margin({ bottom: 40 })
.onClick(() => { router.pushUrl({ url: 'pages/SettingsPage' }) })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
5.2 技术要点解析
1. @Entry 和 @Component 装饰器
@Entry:标记该组件为页面入口,代表这是一个完整的页面@Component:声明这是一个可复用的 ArkTS 组件
两者必须同时使用才能构成一个页面。
2. 路由跳转 --- @ohos.router
鸿蒙 ArkTS 的页面跳转通过 router 模块实现:
typescript
import router from '@ohos.router';
// 带参数跳转
router.pushUrl({
url: 'pages/GamePage',
params: { difficulty: 'easy' }
});
接收参数:
typescript
const params = router.getParams() as Record<string, Object>;
const difficulty = params['difficulty'] as string;
严格模式下接口定义:由于 ArkTS 严格模式不允许未类型化的对象字面量,我们需要提前定义接口:
typescript
interface RouteOpt {
url: string;
params?: Object;
}
interface DiffParams {
difficulty: string;
}
这样在 onClick 中创建对象时类型就是明确的。
3. 链式调用风格
ArkTS 组件广泛使用链式调用(Builder Pattern)来设置属性:
typescript
Button('简单')
.width('80%')
.height(52)
.backgroundColor($r('app.color.primary'))
.borderRadius(26)
.fontColor(Color.White)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 16 })
.onClick(() => { /* ... */ })
这种写法简洁且可读性强,每个方法调用返回组件本身,可以继续设置下一个属性。
4. 描边按钮的设计
"每日挑战"按钮使用了不同的设计------描边样式(outline),与实心按钮形成视觉对比,起到引导用户注意的作用:
typescript
Button($r('app.string.daily_challenge'))
.backgroundColor(Color.White) // 白色背景
.fontColor($r('app.color.primary')) // 主题色文字
.border({ width: 2, color: $r('app.color.primary') }) // 主题色边框
六、module.json5 配置
entry/src/main/module.json5 是模块的核心配置,用于注册 Ability 和扩展能力:
json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"],
"pages": "$profile:main_pages", // 引用页面路由配置
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false
}
]
}
}
几个关键字段:
mainElement:指定入口 AbilitydeviceTypes:支持的设备类型,当前仅phoneskills+entities/actions:声明为桌面应用(显示在桌面图标)extensionAbilities:扩展能力,这里是备份功能
七、ArkTS 严格模式注意事项
鸿蒙 ArkTS 在 API 9+ 启用了严格模式,有两个常见规则需要特别注意:
7.1 arkts-no-untyped-obj-literals
对象字面量必须有显式类型声明,不能写:
typescript
// ❌ 错误:无法推断类型
router.pushUrl({ url: 'pages/GamePage', params: { difficulty: 'easy' } });
必须提前定义接口:
typescript
// ✅ 正确:显式类型
interface RouteOpt {
url: string;
params?: Object;
}
let opt: RouteOpt = { url: 'pages/GamePage', params: { difficulty: 'easy' } };
router.pushUrl(opt);
7.2 arkts-no-noninferrable-arr-literals
数组字面量必须可推断类型,解决方式是将数组定义为独立变量:
typescript
// ✅ 正确
let achievements: Achievement[] = [
{ id: 1, title: '成就A', /* ... */ },
{ id: 2, title: '成就B', /* ... */ }
];

八、小结与预告
本篇我们完成了:
- ✅ 鸿蒙 Stage 模型项目创建与结构理解
- ✅ EntryAbility 生命周期学习
- ✅ 页面路由注册与跳转
- ✅ 资源文件管理(string/color/float)
- ✅ 首页 UI 开发(难度选择 + 底部导航)
- ✅ ArkTS 严格模式注意事项
下一篇我们将进入这个项目的核心------数独游戏引擎的开发(GamePage),包括:
- 9×9 棋盘的 Grid 布局
- 数独题目的动态生成算法
- 单元格的选择、高亮和交互
- 计时器与游戏状态管理
敬请期待!
项目地址:本系列所有代码基于 HarmonyOS Next + ArkTS + Stage 模型,使用 DevEco Studio 开发,API 23 编译。