HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二):Stage模型与“一次开发,多端部署”的骨架

摘要:在上一篇中,我们跑通了 API 23 的《灵犀厨房》首个页面。但“全场景”的秘密武器到底藏在哪里?答案就是 Stage 模型。今天,我们就来彻底拆解 Stage 模型的核心概念、项目结构,并依据官方最新文档,为《灵犀厨房》搭建一套能自适应手机、平板和智慧屏的“多端部署”骨架。

一、为什么 Stage 模型是“全场景”的基石?

很多从 Android 或 iOS 转过来的开发者,看到 EntryAbility 可能会本能地把它当成 Activity。这是一个巨大的误解。

HarmonyOS 6.1.0 主推的 Stage 模型,核心设计思想是:将应用组件(Ability)从窗口(Window)中彻底解耦

  • FA 模型(历史):高度绑定 UI 界面,组件间跳转规则复杂,不同设备适配极其痛苦。
  • Stage 模型(现在)
    • 应用:由 1 个或多个 Module 组成,实现功能解耦。
    • Module:独立的编译、运行单元。比如把“菜谱浏览”做成 Feature Module。
    • UIAbility:负责创建窗口并与用户交互,但它不直接等于一个页面。一个 UIAbility 可在不同设备上,通过窗口管理,以完全不同的形态呈现。

一句话总结:我们写的《灵犀厨房》不再是“手机 App”,而是一个可以按需解构、组合、流转的原子化能力集合

二、解剖《灵犀厨房》Stage 模型项目骨架

让我们重新审视 LingxiKitchen 项目,你会发现很多为全场景而生的“暗门”。

2.1 核心配置:app.json5module.json5

  • AppScope/app.json5(应用级配置)
    这是整个应用的“身份证”。在 HarmonyOS 6.1.0 中,我们按官方属性进行配置:

    {
      "app": {
        "bundleName": "com.annan.lingxikitchen",
        "vendor": "annan",
        "versionCode": 1000000,
        "versionName": "1.0.0",
        "buildVersion": "1",
        "icon": "$media:layered_image",
        "label": "$string:app_name",
        "debug": true,
        "bundleType": "app",
        "description": "$string:app_desc",
        "multiAppMode": {
          "multiAppModeType": "appClone",
          "maxCount": 2
        }
      }
    }
    

    核心属性解读

    • bundleName:应用唯一标识,必须三段以上,采用反域名格式,这是应用的“户口名”。
    • bundleType:我们设为 app,代表这是一个常规应用;未来做元服务时,会改成 atomicService
    • debug:开发期设为 true,方便调错;正式发布前一定要改回 false
    • versionCode & versionName:前者为正整数,数值越大版本越新,用于系统判断;后者是给用户看的版本字符串(如 1.0.0)。
    • buildVersionAPI 23 新增标签,建议采用 A.B.C 三段式,让我们能更精细地追踪构建。
    • multiAppMode:配置应用分身,让《灵犀厨房》理论上支持双开,一个用作家庭版,一个用作个人版。
    • 特别注意minAPIVersiontargetAPIVersion 由 IDE 编译时自动生成,此处无需也没必要手动配置distributedNotificationEnabled 已从 API 9 废弃,不再生效,移除它。
    • 详情请查看官网文档app.json5配置文件
    • 注意:description的值需要在DevEco中配置一个,如下所示:
      在这里插入图片描述
  • entry/src/main/module.json5(模块级配置)
    这是《灵犀厨房》当前唯一主模块的“户口本”,我们的多端适配就从这里开始:

    {
      "module": {
        "name": "entry",
        "type": "entry",
        "description": "$string:module_desc",
        "mainElement": "EntryAbility",
        "deviceTypes": [
          "phone",
          "tablet",
          "tv"
        ],
        "deliveryWithInstall": true,
        "installationFree": false,
        "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,
            "metadata": [
              {
                "name": "ohos.extension.backup",
                "resource": "$profile:backup_config"
              }
            ]
          }
        ]
      }
    }
    

    核心属性解读(基于官方文档支持的属性)

    • deviceTypes这是多端适配的第一步。加入 "tablet""tv",意味着《灵犀厨房》声明支持在这些设备上运行。仅是系统允许还不够,后面必须辅以布局适配。
    • startWindowIcon:冷启动时,鸿蒙会立刻显示这张图,产生“秒开”的错觉,是官方推荐的重要优化项。
    • installationFree:用于元服务免安装,常规应用我们保持 false
    • 详情请查看官网文档module.json5配置文件

    需要你特别留意的四个关键点

    1. installationFree:开发者只读,编译时自动决定
      根据官方说明,这个标签手动配置不生效。当应用为元服务(bundleType: "atomicService")时,它会被强制设为 true;常规应用则强制为 false。我们当前只需保持 false,无需干预。
      2. pages:并非“页面文件夹”,它是路由清单
      pages 对应的 $profile:main_pages,实际指向的是 resources/base/profile/main_pages.json 文件。这个文件里才会一一列出 pages/Indexpages/RecipeDetail 等具体页面路径。若你新建了页面但没在此注册,路由将无法跳转。
      3. deviceTypes:不仅是声明,更是系统分发的依据
      我们配了 "phone""tablet""tv",意味着《灵犀厨房》会在这些设备上被允许运行。但系统允许 ≠ 体验好,后续我们必须为它们分别做布局适配。
      4. 废弃标签勿碰:uiSyntaxsrcEntrance
      它们从 API 9 起就被废弃了。本篇没有使用这些过时属性,你后续在其他项目或旧教程中看到时,可以直接忽略。

    那 API 23 下新增了哪些可用的 module.json5 标签?
    结合你的官方说明,虽然我们现阶段暂未启用,但你得知道它们的存在和用途,方便后续扩展:

    • shareFiles(API 23 新增):配置应用文件的安全分享范围,是鸿蒙对用户隐私保护的进一步强化。
    • easyGo(API 23 新增):用于实现平行视界分栏等产品定制能力,平板适配时会用到。
    • executableBinaryPaths(API 24 新增):我们目前基于 API 23,暂不涉及,先行了解即可。

    有了对 app.json5module.json5 的深度理解,我们的《灵犀厨房》骨架才算真正搭建得坚实。下面,就进入“多设备适配”的实战环节。

2.2 生命周期洞察:EntryAbility.ets

打开 entryability/EntryAbility.ets,这就是《灵犀厨房》在移动设备上的“主窗口经理”:

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (err) {
      hilog.error(DOMAIN, 'LingxiKitchen', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'LingxiKitchen', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'LingxiKitchen', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(DOMAIN, 'LingxiKitchen', '%{public}s', 'Ability onBackground');
  }
}

核心要点

  1. UIAbility 不等于 UI 界面。它本身没视图,管理器 WindowStage 才负责调度。
  2. 前后台监听onForegroundonBackground 是实现“流转恢复”逻辑的关键。比如用户做饭时,手机切后台,手表切前台自动继续计时。

三、实战:搭建多设备适配骨架

清晰的 Stage 模型和配置之后,要让《灵犀厨房》能在手机、平板、智慧屏上运行得游刃有余,我们需掌握资源限定词窗口适配

3.1 资源限定词:一套代码,多套皮肤

HarmonyOS 的资源匹配机制,能让应用根据设备类型、语言、屏幕密度等自动选用最合适的资源。base 目录是默认存在的,里面存放通用资源;当我们需要针对特定设备做差异化时,就在 resources 下创建限定词目录

限定词目录的命名必须遵循官方规则:语言_文字_国家或地区-横竖屏-设备类型-颜色模式-屏幕密度。我们这里只为平板做适配,因此只需创建一个设备类型限定词目录 tablet

操作步骤

  1. 手机默认资源(base 目录)
    打开 entry/src/main/resources/base/element/string.json,修改如下:

     {
       "string": [
         {
           "name": "module_desc",
           "value": "灵犀厨房手机版"
         },
         {
           "name": "EntryAbility_desc",
           "value": "灵犀厨房主入口"
         },
         {
           "name": "EntryAbility_label",
           "value": "灵犀厨房"
         }
       ]
     }
    
  2. 平板专用资源(tablet 限定词目录)
    resources 目录上右键,选择 New > Resource Directory,选择限定词 Device typetablet,资源组类型选择 Element,IDE 会自动生成 tablet/element 目录。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    然后在该目录下新建 string.json(完整文件路径entry/src/main/resources/tablet/element/string.json):

     {
      "string": [
        {
          "name": "module_desc",
          "value": "灵犀厨房平板大屏版"
        },
        {
          "name": "EntryAbility_label",
          "value": "灵犀厨房 HD"
        }
      ]
     }
    

    验证:在 Phone 模拟器上桌面图标显示“灵犀厨房”,应用内描述为“手机版”;在 Tablet 模拟器上桌面图标显示“灵犀厨房 HD”,描述为“平板大屏版”。系统根据设备类型自动匹配 tablet 限定词目录,找不到的资源则回退到 base。

    资源匹配规则补充
    当应用请求某个资源时,系统会按照 MCC/MNC > 区域(语言、文字、国家/地区) > 横竖屏 > 设备类型 > 颜色模式 > 屏幕密度 的优先级,寻找最匹配的限定词目录。只有找不到时,才会使用 base 中的资源。rawfileresfile 目录不参与匹配,里面的文件会原样打包。
    平板模拟器正常运行:
    在这里插入图片描述

3.2 窗口尺寸自适应(ArkTS 响应式布局)

不同设备屏幕形态天差地别,纯靠限定词文件夹不够灵活。ArkTS 提供强大的断点系统来实时感知窗口变化。

Index.ets 中,我们改写代码,让《灵犀厨房》首屏能智能识别横竖屏并改变布局(注意:在模拟器中没有横竖屏按钮,需要使用多屏功能来验证):

import { window } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State appName: string = '灵犀厨房';
  @State slogan: string = '你的AI私人厨艺助手';
  @State currentBreakpoint: string = 'sm';

  private windowClass: window.Window | null = null;
  private sizeChangeCallback: ((size: window.Size) => void) | null = null;

  // 根据窗口宽度手动计算断点(参照官方推荐阈值)
  private getBreakpoint(width: number): string {
    if (width >= 840) return 'lg';
    if (width >= 600) return 'md';
    return 'sm';
  }

  async aboutToAppear(): Promise<void> {
    try {
      this.windowClass = await window.getLastWindow(getContext(this));
      if (this.windowClass) {
        const rect = this.windowClass.getWindowProperties().windowRect;
        console.info('初始窗口宽度: ' + rect.width + ', 高度: ' + rect.height);

        this.currentBreakpoint = this.getBreakpoint(rect.width);
        console.info('初始断点: ' + this.currentBreakpoint);

        this.sizeChangeCallback = (size: window.Size): void => {
          console.info('【回调触发】新窗口宽度: ' + size.width);
          const bp = this.getBreakpoint(size.width);
          this.currentBreakpoint = bp;
          console.info('【回调触发】当前断点切换为: ' + bp);
        };
        this.windowClass.on('windowSizeChange', this.sizeChangeCallback);
        console.info('已注册 windowSizeChange 监听');
      }
    } catch (err) {
      console.error('获取窗口实例失败', JSON.stringify(err));
    }
  }

  aboutToDisappear(): void {
    if (this.windowClass && this.sizeChangeCallback) {
      this.windowClass.off('windowSizeChange', this.sizeChangeCallback);
    }
  }

  build() {
    Flex({
      direction: this.currentBreakpoint === 'sm' ? FlexDirection.Column : FlexDirection.Row,
      wrap: FlexWrap.NoWrap,
      justifyContent: FlexAlign.Center,
      alignItems: ItemAlign.Center
    }) {
      Column() {
        Text('🍳')
          .fontSize(this.currentBreakpoint === 'sm' ? 80 : 120)
        Text(this.appName)
          .fontSize(this.currentBreakpoint === 'sm' ? 36 : 48)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF6B35')
      }
      .margin({ bottom: this.currentBreakpoint === 'sm' ? 30 : 0 })

      Column({ space: 15 }) {
        Text(this.slogan)
          .fontSize(18)
          .fontColor('#999999')

        Button('开始点菜')
          .fontSize(18)
          .backgroundColor('#FF6B35')
          .borderRadius(24)
          .padding({ left: 40, right: 40 })
      }
      .margin({ left: this.currentBreakpoint === 'sm' ? 0 : 40 })
    }
    .height('100%')
    .width('100%')
    .backgroundColor('#FFF8F0')
  }
}

验证实录:我们在 Phone 模拟器的多屏模式下,将《灵犀厨房》拖拽到不同屏幕,Log 输出如下:
初始窗口宽度: 1280, 高度: 2832
初始断点: lg
已注册 windowSizeChange 监听
【回调触发】新窗口宽度: 1320
【回调触发】当前断点切换为: lg
在这里插入图片描述

注意:单 Phone 模拟器旋转屏幕时,窗口像素宽高不变,因此该事件不会触发(不是代码问题)。要体验完整响应式效果,你可以:

  • 分屏/多窗模式:手动缩小窗口宽度,断点将实时切换(md / sm)。
  • 平板模拟器:横竖屏切换通常伴随窗口尺寸变化。
  • 真机:旋转屏幕时绝大多数机型会触发 windowSizeChange。
  • 预览器:直接点击横竖屏切换按钮,预览器会模拟断点变化。

四、本阶段总结与下篇预告

今天,我们将《灵犀厨房》的“骨架”——Stage 模型彻底梳理了一遍。你学到了:

  • Stage 模型的核心理念:应用、Module、UIAbility、Window 的分层与职责。
  • 关键配置文件的正确写法:严格依据 HarmonyOS 6.1.0 官方文档,逐项解析 app.json5module.json5
  • 多设备适配实战deviceTypes 声明、资源限定词、ArkTS 断点响应式布局。

下篇预告:基础打牢了,是时候真正写代码了!下一篇《ArkTS 高效开发:TypeScript 核心与 API 23 新规》,我将带你掌握 ArkTS 语言精髓及官方推荐的最新语法规范。


粉丝专属福利:为感谢大家支持,点赞 + 收藏 本专栏任意文章,并在评论区留言“纯血鸿蒙,我准备好了”,私信我即可领取《HarmonyOS 6.0 安全技术白皮书》电子版!

本文源码地址https://gitee.com/sulongannababy/lingxi-kitchen (第二章代码已同步更新)
专栏目录点击查看全部40篇文章预告

如果你发现本文还有任何不严谨之处,欢迎随时指出,我们一起共建最优质的 HarmonyOS 6.1 学习内容!如果觉得有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬,我们下一篇见~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐