鸿蒙原生应用实战(一):项目创建与首页开发 — 从零搭建数独游戏

鸿蒙原生应用实战(一):项目创建与首页开发 --- 从零搭建数独游戏

前言

随着鸿蒙生态的快速发展,越来越多的开发者开始投身鸿蒙原生应用开发。本系列文章将以一款经典数独游戏为实战项目,从零开始带你体验鸿蒙原生应用(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 模型的优势非常明显:

  1. 组件化设计 :Ability 和 UI 分离,每个 Ability 有独立的 module.json5 配置
  2. 进程管理:支持多实例和进程级隔离
  3. 后台任务:更规范的后台任务管理机制
  4. 生命周期清晰onCreateonWindowStageCreateonForegroundonBackgroundonDestroy

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

首页是用户打开应用后看到的第一个界面。我们的数独首页包含:

  1. 标题区域(emoji + 应用名 + 副标题)
  2. 四个难度选择按钮(简单/中等/困难/每日挑战)
  3. 底部导航链接(排行榜/统计/设置)

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:指定入口 Ability
  • deviceTypes:支持的设备类型,当前仅 phone
  • skills + 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 编译。

相关推荐
风满城332 小时前
【鸿蒙原生应用开发实战】第四篇:相册与提醒——AlbumPage + ReminderPage 完整实现
华为·harmonyos
不羁的木木2 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第三篇:实战案例——单手操作优化
华为·harmonyos
浮芷.3 小时前
HarmonyOS 6.1 沉浸式光感效果-样式切换效果问题解决方案-鸿蒙PC方向
华为·harmonyos·鸿蒙
木咺吟3 小时前
鸿蒙原生应用实战(三):表单交互与搜索筛选——添加包裹、搜索过滤与公司管理
华为·harmonyos
xcLeigh3 小时前
鸿蒙平台 gThumb 图片查看器适配实战:从 Linux GTK 到 Electron 鸿蒙壳工程
linux·electron·harmonyos·gnome·桌面环境·gthumb
金启攻3 小时前
鸿蒙原生应用开发实战(四):复杂页面与交互体验——鱼种百科、天气详情与钓点详情
harmonyos
lqj_本人4 小时前
鸿蒙pc:Hoppscotch-hoppscotch-ohos适配全记录
华为·harmonyos
xcLeigh4 小时前
鸿蒙PC平台 imv 图片查看器适配实战:极简主义设计的 Electron 迁移
华为·electron·harmonyos·鸿蒙·imv·图片操作·web_engine
不羁的木木4 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第四篇:进阶应用——横屏游戏手柄模式
游戏·华为·harmonyos