在这里插入图片描述

每日一句正能量

真正的高手都是专注把一件事做到极致的人。
高手不靠做很多事取胜,而靠把一件事挖到别人挖不到的深度。极致不是宽度,是深度;不是选项多,是放弃多。

导读

*元服务(Atomic Service)是鸿蒙生态中"免安装、即用即走"的轻量服务形态,也是 7.0 意图框架的核心入口之一。在校企合作的实训课程中,我通常会用"天气服务"作为元服务的入门项目——它需求清晰、数据实时、天然适合卡片化展示,且能很好地演示跨设备流转能力。本文将完整记录从零开始构建一个 HarmonyOS 7.0 跨设备天气元服务的全过程,涵盖工程创建、卡片设计、主页面开发、天气数据获取、跨设备流转和分布式数据同步六大环节。所有代码基于 API 26(7.0)风格编写,可直接在 DevEco Studio 4.2 中运行。


一、项目创建与工程结构

1.1 创建元服务工程

打开 DevEco Studio 4.2,选择 File → New → Create Project,在模板列表中选择 “Atomic Service”

工程配置:

  • Project name:WeatherAtom
  • Bundle name:com.example.weatheratom
  • Compile SDK:API 26(7.0)
  • Model:Stage

1.2 工程结构说明

创建完成后,工程目录如下:

WeatherAtom/
├── entry/src/main/
│   ├── ets/
│   │   ├── entryability/
│   │   │   └── EntryAbility.ets          # 主入口 Ability
│   │   ├── formability/
│   │   │   └── WeatherFormAbility.ets    # 卡片 FormProvider
│   │   ├── pages/
│   │   │   └── Index.ets                 # 主页面
│   │   ├── components/
│   │   │   ├── WeatherCard.ets           # 天气卡片 UI 组件
│   │   │   └── ForecastList.ets          # 未来预报列表
│   │   ├── model/
│   │   │   ├── WeatherData.ets           # 天气数据模型
│   │   │   └── WeatherService.ets        # 网络请求服务
│   │   └── distributed/
│   │       └── WeatherSync.ets           # 分布式数据同步
│   └── resources/
│       ├── base/layout/weather_form.json  # 卡片布局描述
│       └── base/media/                   # 天气图标资源
└── entry/src/main/module.json5            # 模块配置(含意图绑定)

图1:WeatherAtom 项目结构图

图片内容说明(中文):树状目录图,根节点"WeatherAtom"下展开 entry/src/main/ets/ 目录,各子目录用不同颜色标注:entryability(蓝色,主入口)、formability(绿色,卡片)、pages(紫色,页面)、components(橙色,组件)、model(灰色,数据)、distributed(青色,同步)。右侧图例标注各目录职责。关键文件用加粗显示。

WeatherAtom

ets/

resources/

module.json5

entryability/
EntryAbility.ets

formability/
WeatherFormAbility.ets

pages/
Index.ets

components/
WeatherCard.ets
ForecastList.ets

model/
WeatherData.ets
WeatherService.ets

distributed/
WeatherSync.ets

layout/
weather_form.json

media/
天气图标


二、元服务配置:module.json5 与意图绑定

元服务的核心配置在 module.json5 中。7.0 新增了意图框架绑定数据分级声明

// entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "atomicService",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "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",
        // 7.0 新增:意图绑定,让系统能根据天气相关意图拉起服务
        "skills": [
          {
            "actions": ["query.weather", "check.forecast"],
            "entities": ["location.current", "time.now"]
          }
        ]
      },
      {
        "name": "WeatherFormAbility",
        "srcEntry": "./ets/formability/WeatherFormAbility.ets",
        "description": "天气服务卡片",
        "label": "$string:WeatherFormAbility_label",
        "forms": [
          {
            "name": "WeatherCard",
            "displayName": "今日天气",
            "description": "实时天气与温度",
            "src": "./ets/formability/WeatherFormAbility.ets",
            "window": {
              "designWidth": 360,
              "autoDesignWidth": true
            },
            "colorMode": "auto",
            "isDefault": true,
            "updateEnabled": true,
            "scheduledUpdateTime": "10:00",
            "updateDuration": 1,
            "defaultDimension": "2*2",
            "supportDimensions": ["2*2", "4*2"]
          }
        ]
      }
    ],
    // 7.0 新增:数据分级
    "dataClassification": {
      "public": ["weather_cache"],
      "internal": ["user_location"],
      "sensitive": []
    }
  }
}

三、天气服务卡片设计

3.1 卡片数据模型

// model/WeatherData.ets
export class WeatherData {
  city: string = '';
  temp: number = 0;           // 当前温度
  condition: string = '';     // 天气状况:晴/雨/多云
  icon: Resource = $r('app.media.sunny');
  humidity: number = 0;       // 湿度
  windLevel: number = 0;      // 风力
  updateTime: string = '';

  constructor(city: string, temp: number, condition: string) {
    this.city = city;
    this.temp = temp;
    this.condition = condition;
    this.updateTime = new Date().toLocaleTimeString();
  }
}

export class ForecastItem {
  day: string = '';
  high: number = 0;
  low: number = 0;
  condition: string = '';
}

3.2 FormProvider 实现

// formability/WeatherFormAbility.ets
import { formProvider } from '@ohos.app.form.formProvider';
import { WeatherService } from '../model/WeatherService';
import { WeatherData } from '../model/WeatherData';

class WeatherFormAbility {
  private service: WeatherService = new WeatherService();

  // 卡片创建时触发
  onAddForm(want: Want): formBindingData.FormBindingData {
    const city = want.parameters?.['city'] as string || '北京';
    return this.createFormData(city);
  }

  // 定时更新触发
  onUpdateForm(formId: string): void {
    const city = this.getCityByFormId(formId);
    const weather = this.service.fetchWeather(city);
    const formData = this.buildFormData(weather);
    formProvider.updateForm(formId, formData);
  }

  private createFormData(city: string): formBindingData.FormBindingData {
    const weather = this.service.fetchWeather(city);
    return formBindingData.createFormBindingData({
      city: weather.city,
      temp: `${weather.temp}°`,
      condition: weather.condition,
      icon: weather.icon.id,
      humidity: `${weather.humidity}%`,
      updateTime: weather.updateTime
    });
  }

  private buildFormData(weather: WeatherData): Object {
    return {
      city: weather.city,
      temp: `${weather.temp}°`,
      condition: weather.condition,
      icon: weather.icon.id,
      humidity: `${weather.humidity}%`,
      updateTime: weather.updateTime
    };
  }
}

export default new WeatherFormAbility();

3.3 卡片布局(weather_form.json)

{
  "layout": {
    "width": "100%",
    "height": "100%",
    "type": "column",
    "padding": "12vp",
    "children": [
      {
        "type": "row",
        "justifyContent": "space-between",
        "children": [
          {
            "type": "text",
            "text": "{{city}}",
            "fontSize": "16fp",
            "fontColor": "#FFFFFF"
          },
          {
            "type": "text",
            "text": "{{updateTime}}",
            "fontSize": "10fp",
            "fontColor": "#CCFFFFFF"
          }
        ]
      },
      {
        "type": "row",
        "marginTop": "8vp",
        "alignItems": "center",
        "children": [
          {
            "type": "image",
            "src": "{{icon}}",
            "width": "40vp",
            "height": "40vp"
          },
          {
            "type": "text",
            "text": "{{temp}}",
            "fontSize": "36fp",
            "fontColor": "#FFFFFF",
            "marginLeft": "12vp"
          }
        ]
      },
      {
        "type": "text",
        "text": "{{condition}} · 湿度 {{humidity}}",
        "fontSize": "12fp",
        "fontColor": "#DDFFFFFF",
        "marginTop": "4vp"
      }
    ],
    "background": {
      "type": "linearGradient",
      "colors": ["#FF6B6B", "#4ECDC4"],
      "direction": "toBottomRight"
    }
  }
}

四、主页面与天气数据层

4.1 网络请求服务

// model/WeatherService.ets
import { http } from '@ohos.net.http';
import { WeatherData, ForecastItem } from './WeatherData';

export class WeatherService {
  private baseUrl: string = 'https://api.weather.example.com/v1';

  async fetchWeather(city: string): Promise<WeatherData> {
    const httpRequest = http.createHttp();
    try {
      const response = await httpRequest.request(
        `${this.baseUrl}/current?city=${encodeURIComponent(city)}`,
        { method: http.RequestMethod.GET, header: { 'Accept': 'application/json' } }
      );

      // 7.0:response.body 为 ArrayBuffer,需解码
      const decoder = new util.TextDecoder();
      const json = JSON.parse(decoder.decodeToString(response.body));

      return new WeatherData(json.city, json.temp, json.condition);
    } catch (e) {
      console.error('获取天气失败:', e);
      // 降级:返回缓存数据或默认数据
      return new WeatherData(city, 25, '晴');
    } finally {
      httpRequest.destroy();
    }
  }

  async fetchForecast(city: string): Promise<ForecastItem[]> {
    // 获取未来 7 天预报
    const httpRequest = http.createHttp();
    const response = await httpRequest.request(
      `${this.baseUrl}/forecast?city=${encodeURIComponent(city)}&days=7`
    );
    const decoder = new util.TextDecoder();
    const json = JSON.parse(decoder.decodeToString(response.body));
    return json.list.map((item: any) => ({
      day: item.day,
      high: item.high,
      low: item.low,
      condition: item.condition
    } as ForecastItem));
  }
}

4.2 主页面 Index.ets

// pages/Index.ets
import { WeatherService } from '../model/WeatherService';
import { WeatherData, ForecastItem } from '../model/WeatherData';
import { WeatherSync } from '../distributed/WeatherSync';

@Entry
@Component
struct WeatherIndex {
  @State weather: WeatherData = new WeatherData('北京', 25, '晴');
  @State forecast: ForecastItem[] = [];
  @State isLoading: boolean = false;
  @State currentCity: string = '北京';

  private service: WeatherService = new WeatherService();
  private sync: WeatherSync = new WeatherSync();

  aboutToAppear() {
    this.loadWeather(this.currentCity);
    // 初始化分布式同步
    this.sync.init('weather_session_001', (data) => {
      this.weather = data;
    });
  }

  async loadWeather(city: string) {
    this.isLoading = true;
    this.weather = await this.service.fetchWeather(city);
    this.forecast = await this.service.fetchForecast(city);
    this.isLoading = false;

    // 同步到分布式数据对象(供其他设备读取)
    this.sync.updateWeather(this.weather);
  }

  build() {
    Column() {
      // 城市选择器
      Row() {
        Text(this.currentCity)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        Button('切换城市')
          .onClick(() => {
            // 简化示例:固定切换几个城市
            const cities = ['北京', '上海', '广州', '深圳'];
            const idx = (cities.indexOf(this.currentCity) + 1) % cities.length;
            this.currentCity = cities[idx];
            this.loadWeather(this.currentCity);
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(16)

      // 当前天气
      WeatherCard({ weather: this.weather })

      // 跨设备流转按钮
      Button('流转到平板/手表')
        .margin({ top: 16 })
        .onClick(() => this.transferToNearbyDevice())

      // 预报列表
      ForecastList({ forecast: this.forecast })

      if (this.isLoading) {
        LoadingProgress()
          .width(40)
          .height(40)
          .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  async transferToNearbyDevice() {
    // 跨设备流转逻辑
    const continuationManager = distributedMissionManager;
    // 7.0:通过意图框架发现附近设备
    const devices = await continuationManager.getContinuationDevices();
    if (devices.length > 0) {
      await continuationManager.continueMission({
        srcDeviceId: '',
        dstDeviceId: devices[0].deviceId,
        missionId: 0
      });
    }
  }
}

五、跨设备流转实现

5.1 分布式任务管理

// distributed/WeatherSync.ets
import { distributed } from '@ohos.data.distributed';
import { WeatherData } from '../model/WeatherData';

export class WeatherSync {
  private g_object: distributed.DistributedObject | null = null;
  private sessionId: string = '';
  private callback: ((data: WeatherData) => void) | null = null;

  async init(sessionId: string, onChange: (data: WeatherData) => void): Promise<void> {
    this.sessionId = sessionId;
    this.callback = onChange;

    // 7.0:创建分布式数据对象,显式声明 QoS
    this.g_object = distributed.createObject(sessionId, {
      city: '',
      temp: 0,
      condition: '',
      updateTime: ''
    }, {
      qos: distributed.QoS.HIGH_RELIABILITY,
      syncMode: distributed.SyncMode.REALTIME
    });

    // 监听远程变更
    this.g_object.on('change', (sessionId: string, fields: Array<string>) => {
      if (this.callback && this.g_object) {
        this.callback(new WeatherData(
          this.g_object.city,
          this.g_object.temp,
          this.g_object.condition
        ));
      }
    });
  }

  updateWeather(weather: WeatherData): void {
    if (this.g_object) {
      // 7.0:批量原子更新
      this.g_object.batchUpdate({
        city: weather.city,
        temp: weather.temp,
        condition: weather.condition,
        updateTime: weather.updateTime
      });
    }
  }

  destroy(): void {
    if (this.g_object) {
      this.g_object.off('change');
      this.g_object = null;
    }
  }
}

5.2 流转时的状态保存与恢复

EntryAbility 中处理流转生命周期:

// entryability/EntryAbility.ets
import { UIAbility, Want } from '@ohos.app.ability.UIAbility';
import { window } from '@ohos.window';

export default class EntryAbility extends UIAbility {
  private weatherSync: WeatherSync = new WeatherSync();

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 判断是否为接续启动
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      // 从 want 参数中恢复状态
      const city = want.parameters?.['city'] as string || '北京';
      AppStorage.setOrCreate('continuedCity', city);
    }
  }

  onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
    // 保存当前状态,供目标设备恢复
    wantParam['city'] = AppStorage.get('currentCity') || '北京';
    wantParam['temp'] = AppStorage.get('currentTemp') || 25;
    return AbilityConstant.OnContinueResult.AGREE;
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        return;
      }
    });
  }
}

六、运行效果与测试要点

6.1 本地运行效果

  1. 桌面卡片:长按桌面空白处 → 添加服务卡片 → 选择"WeatherAtom → 今日天气",卡片显示当前城市、温度、天气图标和湿度,背景为渐变色彩;
  2. 主页面:点击卡片进入全功能页面,展示当前天气 + 7 日预报 + 城市切换按钮;
  3. 实时更新:卡片每 30 分钟自动刷新(受 7.0 限流策略约束)。

6.2 跨设备流转测试

  1. 确保手机和平板登录同一华为账号,开启蓝牙和 WiFi;
  2. 在手机端打开天气元服务,查看北京天气;
  3. 点击"流转到平板/手表",在超级终端中选择平板;
  4. 平板上自动打开天气服务,显示与手机一致的城市和温度数据;
  5. 在手机端切换城市到上海,观察平板端是否自动同步更新。

图2:WeatherAtom 跨设备天气服务运行架构图

图片内容说明(中文):三个设备(手机、平板、手表)呈三角形排列。手机内标注"主控端:查询天气→本地显示→分布式同步",平板内标注"协同端:接收流转→恢复状态→显示详情",手表内标注"轻量端:仅显示温度+天气图标"。三个设备之间有双向箭头,标注"分布式数据对象实时同步"。底部说明:同一sessionId,数据变更自动推送到所有组网设备。

手表

平板

手机

session: weather_session_001
实时同步

精简字段同步

查询天气 API

本地显示

分布式同步

接收流转

恢复状态

显示详情

轻量卡片

温度+图标

6.3 关键测试 checklist

测试项 通过标准
卡片添加 桌面可正常添加 2×2 和 4×2 两种尺寸卡片
数据刷新 卡片定时更新,主页面下拉刷新成功
城市切换 切换城市后天气数据正确更新
跨设备流转 任务可从手机流转到平板,状态不丢失
分布式同步 一端切换城市,另一端 1 秒内同步
弱网降级 网络断开时显示缓存数据,不崩溃
后台更新 卡片在后台仍可定时刷新(受 7.0 限流约束)

七、结语

通过这个 WeatherAtom 项目,我们完整走通了 HarmonyOS 7.0 元服务的核心开发链路:从工程配置到意图绑定,从卡片设计到主页面开发,从网络请求到跨设备流转,从分布式数据同步到状态保存恢复。

元服务的开发哲学与传统应用不同——它追求"最小可用、最快触达、最轻交互"。在 7.0 的意图框架加持下,未来的天气元服务甚至不需要用户主动打开:当用户问小艺"今天会下雨吗",系统可以直接从桌面弹出天气卡片作答。

对于高校学生开发者,建议将本文项目作为元服务开发的"Hello World"——在理解完整链路后,可以尝试替换天气数据源、增加空气质量指数、接入意图框架的更多触发条件,逐步构建出属于自己的鸿蒙元服务作品。


转载自:https://blog.csdn.net/u014727709/article/details/162933111
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐