案例简介:本案例使用 DevEco Studio 与鸿蒙本地模拟器开展开发调试,依托华为云码道实现代码自动生成,并集成华为云 MaaS 大模型服务,完成鸿蒙原生健身助手应用的全流程开发,实现编码提效与 AI 智能的融合落地。

一、概述

1.1 案例介绍

本案例使用 DevEco Studio 与鸿蒙本地模拟器开展开发调试,依托华为云码道实现代码自动生成,并集成华为云 MaaS 大模型服务,完成鸿蒙原生健身助手应用的全流程开发,实现编码提效与 AI 智能的融合落地。

案例技术选型:

  • 华为云码道(CodeArts)代码智能体:一个理解项目需求,懂得编码之道,善用百器的实干派AI研发专家,开启你的编码自动驾驶模式。本案例中作为核心开发工具,通过智能体模式快速构建鸿蒙原生应用代码。

  • DevEco Studio是 HarmonyOS 应用及服务的集成开发环境(IDE),提供了一站式的开发平台,包括代码编辑、编译构建、代码调试、性能调优、模拟器、应用测试等能力。

  • DevEco Studio提供了模拟器(Emulator),为开发者提供了运行和调试HarmonyOS应用/元服务的便捷方式。模拟器还原了真实设备的基本功能,如屏幕旋转、音量调节、模拟的硬件传感器和指定设备的位置等。这使得您无需拥有不同类型的物理设备,就可以在各种虚拟环境中轻松测试您的应用程序。在某些情况下,在模拟器上进行应用测试,相比于在实际物理设备上的测试,有着更快速、更高效的体验。例如,模拟器提供了摇一摇的操作模拟,让您能够轻松触发摇一摇功能。通过DevEco Studio提供的模拟器,您可以更灵活、更高效地进行应用开发和调试,提升您的开发体验与效率。

  • MaaS(MaaS模型即服务)是华为云面向AI开发者推出的一站式大模型开发平台,支持开发者一键体验大模型能力,快速构建大模型应用。Mass平台提供大模型训练、推理、部署、管理、监控等全生命周期管理能力,帮助开发者快速构建大模型应用,加速AI开发。

1.2 适用对象

  • 企业
  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计90分钟。

1.4 案例流程

说明:

  1. 开发者下载安装DevEco Studio并在DevEco Studio中安装配置华为云码道;
  2. 基于华为云码道接入MaaS大模型,自动生成健身助手应用代码;
  3. 用户创建并连接本地模拟器;
  4. 使用模拟器调试运行健身助手应用代码。

1.5 资源总览

本案例预计花费0元。

资源名称 规格 单价(元)
华为云码道(CodeArts)代码智能体 体验版 免费
ModelArts Studio大模型(DS/K2/Q3等 GLM-5.1 0.00
DevEco Studio 6.0.0 Release 免费
模拟器(Emulator) HarmonyOS 6.0.0 免费

二、基础环境与资源准备

2.1 下载安装DevEco Studio

2.2 创建HarmonyOS工程

2.3 创建本地模拟器

2.4 DevEco Studio中安装华为云码道(CodeArts)代码智能体

2.1 ~ 2.4节请参考案例《简记APP:鸿蒙原生记账应用全流程开发实战》中的“二、基础环境与资源准备 2.1 ~ 2.4”章节,完成DevEco Studio的下载安装、HarmonyOS工程创建、本地模拟器的创建及安装华为云码道(CodeArts)代码智能体。

2.5 领取华为云MaaS平台大模型Tokens福利

参考案例《华为开发者空间 - ModelArts Studio大模型通用代金券领取使用指导》中的“二、 开通MaaS平台大模型”章节内容领取代金券,获取到模型的API地址、模型名称和API Key

注意:记录API Key、API地址以及模型名称留作后面步骤使用。

三、鸿蒙原生健身应用代码实践

3.1 华为云码道赋能代码开发

打开华为云码道(CodeArts)代码智能体,对话框中输入以下提示词:

在当前项目目录下,根据以下需求文档,完成鸿蒙原生健身应用代码开发:

1、整体需求概述

用户在前端页面录入个人健身相关信息(性别、身高、年龄、当前体重、目标体重等),确认信息无误后点击「获取私教建议」按钮,前端收集完整用户参数并传递至后端服务;后端调用华为云MaaS API接口,将用户信息封装为智能分析请求,接收AI返回的专业化私教建议内容,最终将结果返回前端展示给用户,完成智能化私教指导服务。

MaaS API参数如下:
API_URL: string = 'YOUR_API_URL';
MODEL_NAME: string = 'YOUR_MODEL_NAME';
API_KEY: string = 'Bearer YOUR_API_KEY';

2、功能需求

2.1 用户信息录入功能
该功能为系统基础前置功能,支持用户完整录入个人身体及健身目标信息,所有输入项做合法性校验,确保传递给AI接口的数据准确有效。具体输入字段及规则如下:
性别:单选输入,选项为男/女,为必输项,不可为空,用于AI适配差异化健身方案(男女身体机能、训练强度、减脂逻辑不同)。
身高:数值输入,单位默认厘米(cm),支持正整数/一位小数,设置合理数值区间(130cm-230cm),禁止空值、负数、非法字符,用于计算用户BMI、适配训练负荷。
年龄:数值输入,支持正整数,区间限制为18-80岁,必输项,禁止非法输入,用于AI结合年龄段适配运动强度、规避运动风险。
当前体重:数值输入,单位默认千克(kg),支持正整数/一位小数,设置合理数值区间(40kg-140kg),必输项,用于分析用户当前体态、肥胖程度。
目标体重:数值输入,单位默认千克(kg),支持正整数/一位小数,必输项,需大于0,用于AI制定减脂、塑形、增重等针对性目标方案。
额外功能:支持用户一键清空输入内容,方便用户重新录入信息;输入框实时校验,非法输入实时提示用户修改。

2.2 私教建议生成功能
该功能为系统核心核心业务功能,基于用户录入的有效信息,触发AI接口调用并生成专属私教建议,完整流程如下:
触发条件:用户完整填写所有必填信息,且全部字段校验通过后,点击「获取私教建议」按钮,触发后续业务逻辑;若存在空值、非法数值,按钮点击无效,页面提示对应输入错误。
参数封装:后端接收前端传递的用户性别、身高、年龄、当前体重、目标体重数据,统一数据格式,封装为符合华为云MaaS API要求的请求参数。
AI接口调用:后端调用华为云MaaS大模型API,将用户身体数据、健身目标作为提示词核心内容,请求AI进行智能分析。
结果接收与解析:后端接收华为云MaaS API返回的结构化文本数据,完成数据解析、去冗余、格式优化,剔除无效内容。
结果前端展示:将处理后的AI私教建议返回前端,以清晰的文本格式展示,内容需涵盖训练计划、饮食建议、作息指导、注意事项、周期目标等详细内容。

2.3 加载与异常提示功能
加载状态:用户点击获取建议按钮后,页面展示加载状态,提示「AI正在生成专属私教建议,请稍候」,避免用户重复点击操作。

3、接口需求

3.1 第三方接口:华为云MaaS API
本系统核心依赖华为云MaaS大模型API实现智能建议生成,接口需求如下:
接口用途:基于用户个人身体数据和体重目标,智能分析并生成个性化健身私教全套建议。
请求方式:遵循华为云MaaS API官方请求协议(HTTPS)。
请求参数:包含用户性别、年龄、身高、当前体重、目标体重,以及固定业务提示词(引导大模型输出专业、详细、可落地的健身私教方案,涵盖训练、饮食、禁忌等维度)。
响应数据:返回纯文本结构化内容,内容需分层清晰,包含适配用户情况的个性化建议,无无关冗余信息。
超时要求:接口请求超时时间不超过15秒,超时则触发异常提示。

3.2 前后端交互接口
提供前端信息提交、AI建议获取接口,支持POST请求,接收用户录入的完整信息,返回最终AI生成的私教建议内容及状态码。

注意:替换常量MODEL_NAME、API_URL、API_KEY。

  • YOUR_API_URL:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的API地址
  • YOUR_MODEL_NAME:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的模型名称
  • YOUR_API_KEY:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的API Key

经过几分钟后,华为云码道帮助我们生成了基础版原始代码。由于模型本身的局限性,生成的代码存在语法错误和逻辑错误,代码优化时间过长,为了减少无效的等待并增加案例的连贯性和趣味性,因此本案例提供鸿蒙原生健身应用源码

下载解压完成后,使用DevEco Studio打开项目源码:

3.2 项目介绍与项目结构

项目介绍:

本项目集成AI能力,用户输入个人信息(性别、身高、年龄、当前体重和目标体重等),点击获取私教建议按钮,调用华为云MaaS API,获取详细的私教建议。

项目结构:

FitnessAssistant:项目名称

  • AppScope > app.json5:应用的全局配置信息。
  • entry:HarmonyOS工程模块,编译构建生成一个HAP包。
    • src > main > ets:用于存放ArkTS源码。

      • bean:大模型返回数据的封装类
      • entryability:应用/服务的入口。
      • entrybackupability:应用提供扩展的备份恢复能力。
      • model:模型返回数据解析并获取。
      • pages:应用/服务包含的页面。
      • utils:工具类,包含日志打印、网络请求、常量配置等工具类。
      • view:自定义组件。
    • src > main > resources:用于存放应用/服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。

    • src > main > module.json5:模块配置文件。

    • build-profile.json5:当前的模块信息 、编译信息配置项,包括buildOption、targets配置等。

    • hvigorfile.ts:模块级编译构建任务脚本。

    • obfuscation-rules.txt:混淆规则文件。混淆开启后,在使用Release模式进行编译时,会对代码进行编译、混淆及压缩处理,保护代码资产。

    • oh-package.json5:用来描述包名、版本、入口文件(类型声明文件)和依赖项等信息。

  • oh_modules:用于存放三方库依赖信息。
  • build-profile.json5:工程级配置信息,包括签名signingConfigs、产品配置products等。其中products中可配置当前运行环境,默认为HarmonyOS。
  • hvigorfile.ts:工程级编译构建任务脚本。
  • oh-package.json5:主要用来描述全局配置。

3.3 核心代码解析

应用主界面:pages/MainPage.ets

智能健身助手的主界面,用户在此界面选择性别、身高、年龄、当前体重、目标体重等信息。点击“获取私教建议”按钮,调用华为云MaaS API,获取详细的私教建议。

MainPage.ets完整代码如下:

import TextCommonComponent from '../view/TextCommonComponent';
import { PersonInfo } from '../bean/PersonInfo';


@Entry
@Component
struct Index {
  pageInfos: NavPathStack = new NavPathStack();
  @State sex: string = "男";
  @State currentHeight: string = "175cm";
  @State currentAge: string = "28岁";
  @State targetWeight: string = "75kg"
  @State currentWeight: string = "65kg";
  @State select: number = 0;
  @State customPopup: boolean = false
  private sexArray: Resource = $r('app.strarray.sex_array');


  build() {
    Column() {
      Navigation((this.pageInfos)) {
        Stack({ alignContent: Alignment.Top }) {
          Image($r('app.media.picture_bg'))
            .width('100%')
            .height(200)
            .objectFit(ImageFit.Fill)

          Text('智能健身助手')
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .margin({ top: 45 })
        }
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
        .width('100%')
        .height(200)

        Column() {
          TextCommonComponent({
            textImage: $r('app.media.icon_sex'),
            title: $r('app.string.sex'),
            content: this.sex,
            onItemClick: () => {
              this.getUIContext().showTextPickerDialog({
                range: this.sexArray,
                selected: this.select,
                canLoop: false,
                onAccept: (value: TextPickerResult) => {
                  this.select = value.index as number;
                  this.sex = value.value as string;
                },
                onChange: (value: TextPickerResult) => {
                  this.select = value.index as number;
                }
              })
            }
          })
        }

        Column() {
          TextCommonComponent({
            textImage: $r('app.media.icon_height'),
            title: $r('app.string.current_height'),
            content: this.currentHeight,
            onItemClick: () => {
              let heightArray: Array = [];
              for (let i = 150; i  {
                  this.select = value.index as number;
                  this.currentHeight = value.value as string;
                },
                onChange: (value: TextPickerResult) => {
                  this.select = value.index as number;
                }
              })
            }
          })
        }

        Column() {
          TextCommonComponent({
            textImage: $r('app.media.icon_age'),
            title: $r('app.string.current_age'),
            content: this.currentAge,
            onItemClick: () => {
              let ageArray: Array = [];
              for (let i = 18; i  {
                  this.select = value.index as number;
                  this.currentAge = value.value as string;
                },
                onChange: (value: TextPickerResult) => {
                  this.select = value.index as number;
                }
              })
            }
          })
        }

        Column() {
          TextCommonComponent({
            textImage: $r('app.media.icon_current'),
            title: $r('app.string.current_weight'),
            content: this.currentWeight,
            onItemClick: () => {
              let weightArray: Array = [];
              for (let i = 40; i  {
                  this.select = value.index as number;
                  this.currentWeight = value.value as string;
                },
                onChange: (value: TextPickerResult) => {
                  this.select = value.index as number;
                }
              })
            }
          })
        }

        Column() {
          TextCommonComponent({
            textImage: $r('app.media.icon_target'),
            title: $r('app.string.target_weight'),
            content: this.targetWeight,
            onItemClick: () => {
              let weightArray: Array = [];
              for (let i = 40; i  {
                  this.select = value.index as number;
                  this.targetWeight = value.value as string;
                },
                onChange: (value: TextPickerResult) => {
                  this.select = value.index as number;
                }
              })
            }
          })
        }

        Button('获取私教建议')
          .width('95%')
          .height('45vp')
          .fontSize('18fp')
          .fontWeight(FontWeight.Medium)
          .backgroundColor('#007DFF')
          .margin({ top: '40vp', bottom: '12vp' })
          .onClick(() => {
            let personInfo: PersonInfo = new PersonInfo(this.sex,this.currentHeight,this.currentAge,this.currentWeight, this.targetWeight);
            this.pageInfos.pushPathByName('detailPage', personInfo);
          })
      }
    }.width("100%").height("100%")

    .backgroundColor('#F1F3F5')
  }
}

应用详情页:pages/DetailPage.ets

调用华为云MaaS API,获取数据、解析数据并展示在该界面上。

DetailPage.ets完整代码如下:

import Logger from '../utils/Logger';
import { PersonInfo } from '../bean/PersonInfo';
import { List } from "@kit.ArkTS";
import UploadingLayout from '../view/UploadingLayout';
import DetailViewModel from '../model/DetailViewModel';

@Builder
export function DetailPageBuilder(name: string, param: Object) {
  DetailPage()
}

@Component
export struct DetailPage {
  pageInfos: NavPathStack = new NavPathStack();
  @State content: string = ""
  @State prompt: string = ""
  @State isUploading: boolean = false;

  formatString(str: string): string {
    // 使用正则表达式找到数字之间的横线,并用特殊标记替换
    let result: string = str.replace(/(\d+(\.\d+)?)-(\d+(\.\d+)?)/g, '$1__TEMP_DASH__$3');
    // 删除所有的 -、#、*
    let cleaned: string = result.replace(/[-#*]/g, '');
    // 恢复数字范围中的横线
    return cleaned.replace(/__TEMP_DASH__/g, '-');
  }

  getParamsPrint() {
    this.isUploading = true
    let jsonString = JSON.stringify(this.pageInfos.getParamByName('detailPage'))
    let personInfoList: List = JSON.parse(jsonString) as List;
    let personInfo: PersonInfo = personInfoList[0] as PersonInfo;
    let age: string = personInfo.currentAge;
    let sex: string = personInfo.sex;
    let height: string = personInfo.currentHeight;
    let currentWeight: string = personInfo.currentWeight;
    let targetWeight: string = personInfo.targetWeight;
    this.prompt =
      `您好,我是一名健身爱好者,年龄:${age}、性别:${sex}、身高:${height}、当前体重:${currentWeight}、目标体重:${targetWeight},请给出健身建议!`;
    Logger.info(`prompt: ${this.prompt}`);

    DetailViewModel.requestModelData(this.prompt).then((content) => {
      Logger.info(`content: ${content}`);
      this.isUploading = false
      this.content = content
    })
  }

  build() {
    Column() {
      NavDestination() {
        Stack() {
          Scroll() {
            Text(this.formatString(this.content))
              .fontSize(18)
              .fontWeight(FontWeight.Medium)
              .width("100%")
              .padding({
                top: 20,
                left: 10,
                right: 10,
                bottom: 20
              })
              .textAlign(TextAlign.Start)
          }
          .height("100%")

          if (this.isUploading) {
            UploadingLayout()
          }
        }
      }
      .onShown(() => {
        this.getParamsPrint();
      })
      .title("健身建议")
      .layoutWeight(HorizontalAlign.Center)
      .width("100%")
      .padding({ top: 40 })
      .hideTitleBar(false)
      .onReady((context: NavDestinationContext) => {
        this.pageInfos = context.pathStack;
      })
    }
  }
}

模型数据获取:HttpHelper.ets

封装网络请求工具类HttpHelper,用于获取模型返回的数据。

HttpHelper.ets完整代码如下:

import { http } from '@kit.NetworkKit';
import { ModelResultBean } from '../bean/ModelResultBean';
import Logger from './Logger';
import { ModelConstants } from './ModelConstants';

class HttpHelper {
  async requestData(prompt: string): Promise {
    // 每一个httpRequest对应一个HTTP请求任务,不可复用
    let httpRequest = http.createHttp();
    // 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
    httpRequest.on('headersReceive', (header) => {
      console.info('header: ' + JSON.stringify(header));
    });

    let options: http.HttpRequestOptions = {
      method: http.RequestMethod.POST,
      // 开发者根据自身业务需要添加header字段
      header: {
        'Content-Type': 'application/json',
        // 把yourApiKey替换成真实的API Key
        "Authorization": `${ModelConstants.API_KEY}`
      },
      // 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定
      extraData: {
        "model": `${ModelConstants.MODEL_NAME}`,
        "messages": [
          { "role": "system", "content": "You are a helpful assistant." },
          { "role": "user", "content": prompt }
        ],
        "stream": false,
        "temperature": 0.6
      },
      // 可选,指定返回数据的类型
      expectDataType: http.HttpDataType.STRING,
      // 可选,默认为true
      usingCache: true,
      // 可选,默认为1
      priority: 1,
      // 可选,默认为60000ms
      connectTimeout: 180000,
      // 可选,默认为60000ms
      readTimeout: 180000,
      // 可选,协议类型默认值由系统自动指定
      usingProtocol: http.HttpProtocol.HTTP1_1,
    }
    let messageResult: string = ''
    try {
      let httpResponse = await httpRequest.request(ModelConstants.API_URL, options)
      let responseCode = httpResponse.responseCode
      let responseResult = httpResponse.result as string
      Logger.info(`responseResult: ${responseResult}`);
      Logger.info(`responseCode: ${responseCode}`);
      if (responseCode == 200) {
        let modelResultBean = JSON.parse(responseResult) as ModelResultBean
        messageResult = JSON.stringify(modelResultBean.choices)
        Logger.info(`messageResult: ${messageResult}`);
      } else {
        messageResult = ""
      }
    } catch (err) {
      Logger.info(`messageResult: ${JSON.stringify(err)}`);
      messageResult = ""
    }
    httpRequest.off('headersReceive');
    // 当该请求使用完毕时,调用destroy方法主动销毁
    httpRequest.destroy();
    return messageResult
  }
}

export default new HttpHelper();

注意:调用MaaS API需要使用网络权限。

entry > src > main > module.json5文件中添加网络请求权限。

"requestPermissions": [
 {
   "name": "ohos.permission.INTERNET"
 }
],

MaaS API常量:ModelConstants.ets

包含模型的API地址、模型名称和API Key。

export class ModelConstants {
  static readonly API_URL: string = 'YOUR_API_URL';
  static readonly MODEL_NAME: string = 'YOUR_MODEL_NAME';
  static readonly API_KEY: string = 'Bearer YOUR_API_KEY';
}

注意:替换常量MODEL_NAME、API_URL、API_KEY。

  • YOUR_API_URL:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的API地址
  • YOUR_MODEL_NAME:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的模型名称
  • YOUR_API_KEY:替换成步骤“2.5 领取华为云MaaS平台大模型Tokens福利”中获取的API Key

3.4 运行体验鸿蒙原生健身应用

打开DevEco Studio项目工程,点击右上角运行按钮,部署鸿蒙应用。

选择性别:

选择身高:

选择年龄:

选择当前体重:

选择目标体重:

点击获取私教建议按钮,调用MaaS API:

获取了详细的健身建议:

至此,码道托管 + MaaS赋能:鸿蒙原生健身应用实践的案例已全部完成。

四、反馈改进建议

如您在案例实操过程中遇到问题或有改进建议,可以到论坛帖评论区反馈即可,我们会及时响应处理,谢谢!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐