📖 鸿蒙NEXT开发实战系列 | 第27篇 | 实战篇 🎯 适合人群:了解元服务基础的开发者 ⏰ 阅读时间:约20分钟 | 💻 开发环境:DevEco Studio 5.0+

上一篇鸿蒙NEXT开发实战系列-26-实战篇-跨设备分布式数据同步实战 下一篇鸿蒙NEXT开发实战系列-28-实战篇-新闻资讯卡片开发实战


前言

万能卡片是 HarmonyOS 最具特色的功能之一,它可以让用户在桌面上直接查看应用的关键信息,无需打开应用。今天我们来实战开发一个天气服务卡片,这个项目将带你掌握:

  • ✅ 服务卡片的完整配置流程

  • ✅ FormExtensionAbility 生命周期管理

  • ✅ 多尺寸卡片适配(2×2 和 2×4)

  • ✅ 定时刷新与数据绑定

  • ✅ 跨设备数据同步

一、项目结构总览

1.1 创建元服务项目

首先在 DevEco Studio 中创建一个新的元服务项目,选择"Empty Ability"模板。

WeatherCard/
├── entry/
│   └── src/
│       └── main/
│           ├── ets/
│           │   ├── entryability/
│           │   │   └── EntryAbility.ets          # 入口Ability
│           │   ├── pages/
│           │   │   └── Index.ets                  # 主页面
│           │   ├── weathercard/
│           │   │   ├── WeatherCard2x2.ets         # 2×2卡片
│           │   │   ├── WeatherCard2x4.ets         # 2×4卡片
│           │   │   └── WeatherFormAbility.ets     # FormExtensionAbility
│           │   ├── services/
│           │   │   └── WeatherService.ets         # 天气数据服务
│           │   └── common/
│           │       └── WeatherData.ets            # 数据模型
│           ├── resources/
│           │   └── base/
│           │       ├── profile/
│           │       │   ├── form_config.json       # 卡片配置
│           │       │   └── weather_config.json    # 天气配置
│           │       └── media/                     # 图标资源
│           └── module.json5                       # 模块配置
└── oh-package.json5

1.2 核心概念说明

在开始编码前,先了解几个核心概念:

概念

说明

FormExtensionAbility

卡片扩展能力,管理卡片生命周期

form_config.json

卡片配置文件,定义尺寸、刷新规则等

formBindingData

卡片数据绑定,用于向卡片传递数据

Want

用于启动卡片和传递参数

二、卡片配置详解

2.1 配置 form_config.json

resources/base/profile/ 目录下创建 form_config.json

{
  "forms": [
    {
      "name": "weather_2x2",
      "description": "天气卡片 2×2",
      "src": "./ets/weathercard/WeatherCard2x2.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "isDefault": true,
      "colorMode": "auto",
      "supportDimensions": ["2*2"],
      "defaultDimension": "2*2",
      "updateEnabled": true,
      "scheduledUpdateTime": "30:00",
      "updateDuration": 30,
      "formConfigAbility": "pages/Index",
      "metadata": {
        "customData": "weather_2x2_custom_data"
      }
    },
    {
      "name": "weather_2x4",
      "description": "天气卡片 2×4",
      "src": "./ets/weathercard/WeatherCard2x4.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "isDefault": false,
      "colorMode": "auto",
      "supportDimensions": ["2*4"],
      "defaultDimension": "2*4",
      "updateEnabled": true,
      "scheduledUpdateTime": "30:00",
      "updateDuration": 30,
      "formConfigAbility": "pages/Index",
      "metadata": {
        "customData": "weather_2x4_custom_data"
      }
    }
  ]
}

配置项说明

  • name: 卡片名称,需要在代码中引用

  • src: 卡片 UI 页面路径

  • supportDimensions: 支持的尺寸,2*2 表示 2 行 2 列

  • updateDuration: 刷新间隔,单位为分钟(30分钟)

  • scheduledUpdateTime: 定时刷新时间点

2.2 配置 module.json5

module.json5 中注册 FormExtensionAbility:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet", "2in1"],
    "deliveryWithInstall": true,
    "installationFree": true,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      },
      {
        "name": "WeatherFormAbility",
        "srcEntry": "./ets/weathercard/WeatherFormAbility.ets",
        "description": "天气服务卡片",
        "icon": "$media:icon",
        "label": "天气卡片",
        "type": "form",
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

三、数据模型与服务

3.1 定义天气数据模型

创建 ets/common/WeatherData.ets

// 天气数据模型
export interface WeatherData {
  // 城市名称
  cityName: string
  // 当前温度
  temperature: number
  // 天气状况
  condition: string
  // 天气图标
  icon: string
  // 湿度
  humidity: number
  // 风速
  windSpeed: string
  // 最高温
  highTemp: number
  // 最低温
  lowTemp: number
  // 更新时间
  updateTime: string
  // 未来几天预报
  forecast: ForecastData[]
}

export interface ForecastData {
  day: string
  icon: string
  highTemp: number
  lowTemp: number
}

// 卡片数据结构
export interface CardData {
  // 基础数据
  title: string
  detail: string
  // 天气相关数据
  cityName: string
  temperature: string
  condition: string
  humidity: string
  windSpeed: string
  highTemp: string
  lowTemp: string
  updateTime: string
  forecast: ForecastData[]
}

3.2 天气数据服务

创建 ets/services/WeatherService.ets

import { WeatherData, ForecastData, CardData } from '../common/WeatherData'
import { preferences } from '@kit.ArkData'
import { distributedKVStore } from '@kit.DistributedService'

const STORE_NAME = 'WeatherStore'
const KEY_WEATHER_DATA = 'weather_data'

export class WeatherService {
  private static instance: WeatherService | null = null
  private kvStore: distributedKVStore.SingleKVStore | null = null

  // 单例模式
  static getInstance(): WeatherService {
    if (!WeatherService.instance) {
      WeatherService.instance = new WeatherService()
    }
    return WeatherService.instance
  }

  // 初始化分布式数据库
  async initKVStore(context: Context): Promise<void> {
    try {
      const kvManager = distributedKVStore.createKVManager({
        bundleName: 'com.example.weathercard',
        context: context
      })

      const options: distributedKVStore.Options = {
        createIfMissing: true,
        encrypt: false,
        backup: false,
        autoSync: true,
        kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION
      }

      this.kvStore = await kvManager.getKVStore(STORE_NAME, options)
    } catch (err) {
      console.error(`Failed to create KVStore: ${err}`)
    }
  }

  // 获取天气数据
  async getWeatherData(): Promise<WeatherData> {
    // 模拟获取天气数据
    // 实际项目中应调用天气 API
    const weatherData: WeatherData = {
      cityName: '北京',
      temperature: 25,
      condition: '晴',
      icon: 'sunny',
      humidity: 60,
      windSpeed: '3级',
      highTemp: 28,
      lowTemp: 18,
      updateTime: this.formatTime(new Date()),
      forecast: [
        { day: '明天', icon: 'cloudy', highTemp: 26, lowTemp: 17 },
        { day: '后天', icon: 'rainy', highTemp: 22, lowTemp: 15 },
        { day: '大后天', icon: 'sunny', highTemp: 27, lowTemp: 19 }
      ]
    }

    // 保存到本地存储
    await this.saveWeatherData(weatherData)

    return weatherData
  }

  // 保存天气数据
  private async saveWeatherData(data: WeatherData): Promise<void> {
    try {
      // 保存到 Preferences
      const context = getContext(this) as Context
      const prefs = await preferences.getPreferences(context, 'weather_prefs')
      await prefs.put(KEY_WEATHER_DATA, JSON.stringify(data))
      await prefs.flush()

      // 同步到分布式数据库
      if (this.kvStore) {
        await this.kvStore.put(KEY_WEATHER_DATA, JSON.stringify(data))
      }
    } catch (err) {
      console.error(`Failed to save weather data: ${err}`)
    }
  }

  // 获取卡片数据
  getCardData(data: WeatherData, dimension: string): CardData {
    if (dimension === '2*2') {
      return {
        title: data.cityName,
        detail: `${data.temperature}° ${data.condition}`,
        cityName: data.cityName,
        temperature: `${data.temperature}°`,
        condition: data.condition,
        humidity: `${data.humidity}%`,
        windSpeed: data.windSpeed,
        highTemp: `${data.highTemp}°`,
        lowTemp: `${data.lowTemp}°`,
        updateTime: data.updateTime,
        forecast: data.forecast
      }
    } else {
      return {
        title: `${data.cityName} ${data.temperature}°`,
        detail: data.condition,
        cityName: data.cityName,
        temperature: `${data.temperature}°`,
        condition: data.condition,
        humidity: `${data.humidity}%`,
        windSpeed: data.windSpeed,
        highTemp: `${data.highTemp}°`,
        lowTemp: `${data.lowTemp}°`,
        updateTime: data.updateTime,
        forecast: data.forecast
      }
    }
  }

  // 格式化时间
  private formatTime(date: Date): string {
    const hours = date.getHours().toString().padStart(2, '0')
    const minutes = date.getMinutes().toString().padStart(2, '0')
    return `${hours}:${minutes}`
  }
}

四、FormExtensionAbility 实现

4.1 创建卡片扩展能力

创建 ets/weathercard/WeatherFormAbility.ets

import { formBindingData, formInfo, formProvider } from '@kit.FormKit'
import { Want } from '@kit.AbilityKit'
import { WeatherService } from '../services/WeatherService'
import { WeatherData, CardData } from '../common/WeatherData'

const TAG = 'WeatherFormAbility'

export default class WeatherFormAbility extends FormExtensionAbility {
  // 卡片被创建时调用
  onCreate(want: Want): formBindingData.FormBindingData {
    console.info(TAG, 'Form onCreate')
    const formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string
    const formName = want.parameters?.[formInfo.FormParam.NAME_KEY] as string
    const dimension = want.parameters?.[formInfo.FormParam.DIMENSION_KEY] as number

    console.info(TAG, `Form created: ${formId}, name: ${formName}, dimension: ${dimension}`)

    // 返回初始数据
    return this.getDefaultFormData(formName, dimension)
  }

  // 卡片被销毁时调用
  onDestroy(formId: string): void {
    console.info(TAG, `Form onDestroy: ${formId}`)
  }

  // 卡片可见时调用
  onVisibilityChange(newStatus: Record<string, number>): void {
    console.info(TAG, `Form visibility changed`)
  }

  // 卡片事件被触发时调用
  onFormEvent(formId: string, message: string): void {
    console.info(TAG, `Form event: ${formId}, message: ${message}`)
    // 处理卡片事件,如点击刷新按钮
    if (message === 'refresh') {
      this.refreshFormData(formId)
    }
  }

  // 卡片更新时调用
  onUpdate(formId: string): void {
    console.info(TAG, `Form onUpdate: ${formId}`)
    this.refreshFormData(formId)
  }

  // 获取默认表单数据
  private getDefaultFormData(formName: string, dimension: number): formBindingData.FormBindingData {
    const dimStr = dimension === 1 ? '2*2' : '2*4'
    const data: CardData = {
      title: '加载中...',
      detail: '获取天气信息',
      cityName: '加载中',
      temperature: '--°',
      condition: '获取中',
      humidity: '--%',
      windSpeed: '--',
      highTemp: '--°',
      lowTemp: '--°',
      updateTime: '--:--',
      forecast: []
    }

    return formBindingData.createFormBindingData(data)
  }

  // 刷新表单数据
  private async refreshFormData(formId: string): Promise<void> {
    try {
      const weatherService = WeatherService.getInstance()
      const weatherData = await weatherService.getWeatherData()
      const formInfoObj = await formProvider.getFormInfo(formId)
      const dimension = formInfoObj?.formConfigProperties?.supportDimensions?.[0] || '2*2'

      const cardData = weatherService.getCardData(weatherData, dimension)

      await formProvider.updateForm(formId, {
        data: JSON.stringify(cardData)
      })

      console.info(TAG, `Form updated: ${formId}`)
    } catch (err) {
      console.error(TAG, `Failed to update form: ${err}`)
    }
  }
}

五、卡片 UI 实现

5.1 2×2 天气卡片

创建 ets/weathercard/WeatherCard2x2.ets

import { formBindingData } from '@kit.FormKit'
import { CardData } from '../common/WeatherData'

@Entry
@Component
struct WeatherCard2x2 {
  @State cityName: string = '加载中'
  @State temperature: string = '--°'
  @State condition: string = '获取中'
  @State humidity: string = '--%'
  @State highTemp: string = '--°'
  @State lowTemp: string = '--°'
  @State updateTime: string = '--:--'
  @State icon: string = '☀️'

  aboutToAppear() {
    // 监听卡片数据变化
    formBindingData.onFormDataChanged(this.onDataChanged.bind(this))
  }

  onDataChanged(data: CardData) {
    if (data) {
      this.cityName = data.cityName || '未知'
      this.temperature = data.temperature || '--°'
      this.condition = data.condition || '未知'
      this.humidity = data.humidity || '--%'
      this.highTemp = data.highTemp || '--°'
      this.lowTemp = data.lowTemp || '--°'
      this.updateTime = data.updateTime || '--:--'
      this.icon = this.getWeatherIcon(data.condition)
    }
  }

  getWeatherIcon(condition: string): string {
    switch (condition) {
      case '晴': return '☀️'
      case '多云': return '⛅'
      case '阴': return '☁️'
      case '雨': return '🌧️'
      case '雪': return '❄️'
      default: return '🌤️'
    }
  }

  build() {
    Column() {
      // 城市名称和温度
      Row() {
        Text(this.cityName)
          .fontSize(14)
          .fontColor('#FFFFFF')
          .opacity(0.9)

        Blank()

        Text(this.updateTime)
          .fontSize(10)
          .fontColor('#FFFFFF')
          .opacity(0.7)
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 12 })

      // 主要温度显示
      Row() {
        Text(this.icon)
          .fontSize(36)

        Column() {
          Text(this.temperature)
            .fontSize(42)
            .fontWeight(FontWeight.Light)
            .fontColor('#FFFFFF')

          Text(this.condition)
            .fontSize(12)
            .fontColor('#FFFFFF')
            .opacity(0.9)
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 8 })
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 8 })

      // 湿度和高低温
      Row() {
        Text(`💧 ${this.humidity}`)
          .fontSize(11)
          .fontColor('#FFFFFF')
          .opacity(0.8)

        Blank()

        Text(`${this.highTemp}/${this.lowTemp}`)
          .fontSize(11)
          .fontColor('#FFFFFF')
          .opacity(0.8)
      }
      .width('100%')
      .padding({ left: 12, right: 12, bottom: 12 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#4FC3F7')
    .borderRadius(16)
    .alignItems(HorizontalAlign.Start)
  }
}

5.2 2×4 天气卡片

创建 ets/weathercard/WeatherCard2x4.ets

import { formBindingData } from '@kit.FormKit'
import { CardData, ForecastData } from '../common/WeatherData'

@Entry
@Component
struct WeatherCard2x4 {
  @State cityName: string = '加载中'
  @State temperature: string = '--°'
  @State condition: string = '获取中'
  @State humidity: string = '--%'
  @State windSpeed: string = '--'
  @State highTemp: string = '--°'
  @State lowTemp: string = '--°'
  @State updateTime: string = '--:--'
  @State icon: string = '☀️'
  @State forecast: ForecastData[] = []

  aboutToAppear() {
    formBindingData.onFormDataChanged(this.onDataChanged.bind(this))
  }

  onDataChanged(data: CardData) {
    if (data) {
      this.cityName = data.cityName || '未知'
      this.temperature = data.temperature || '--°'
      this.condition = data.condition || '未知'
      this.humidity = data.humidity || '--%'
      this.windSpeed = data.windSpeed || '--'
      this.highTemp = data.highTemp || '--°'
      this.lowTemp = data.lowTemp || '--°'
      this.updateTime = data.updateTime || '--:--'
      this.icon = this.getWeatherIcon(data.condition)
      this.forecast = data.forecast || []
    }
  }

  getWeatherIcon(condition: string): string {
    switch (condition) {
      case '晴': return '☀️'
      case '多云': return '⛅'
      case '阴': return '☁️'
      case '雨': return '🌧️'
      case '雪': return '❄️'
      default: return '🌤️'
    }
  }

  getForecastIcon(condition: string): string {
    switch (condition) {
      case 'sunny': return '☀️'
      case 'cloudy': return '⛅'
      case 'rainy': return '🌧️'
      default: return '🌤️'
    }
  }

  @Builder
  ForecastItem(item: ForecastData) {
    Column() {
      Text(item.day)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .opacity(0.9)

      Text(this.getForecastIcon(item.icon))
        .fontSize(20)
        .margin({ top: 4, bottom: 4 })

      Text(`${item.highTemp}°`)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Medium)

      Text(`${item.lowTemp}°`)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .opacity(0.7)
    }
    .width('33%')
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Column() {
      // 顶部区域:城市和更新时间
      Row() {
        Text(this.cityName)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')

        Blank()

        Text(`更新于 ${this.updateTime}`)
          .fontSize(10)
          .fontColor('#FFFFFF')
          .opacity(0.7)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16 })

      // 中部区域:天气详情
      Row() {
        Column() {
          Text(this.icon)
            .fontSize(48)

          Text(this.condition)
            .fontSize(12)
            .fontColor('#FFFFFF')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Center)
        .width('30%')

        Column() {
          Text(this.temperature)
            .fontSize(56)
            .fontWeight(FontWeight.Light)
            .fontColor('#FFFFFF')

          Row() {
            Text(`💧 ${this.humidity}`)
              .fontSize(11)
              .fontColor('#FFFFFF')
              .opacity(0.8)

            Text('   ')

            Text(`💨 ${this.windSpeed}`)
              .fontSize(11)
              .fontColor('#FFFFFF')
              .opacity(0.8)
          }
          .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .width('40%')

        Column() {
          Text(`↑ ${this.highTemp}`)
            .fontSize(12)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Medium)

          Text(`↓ ${this.lowTemp}`)
            .fontSize(12)
            .fontColor('#FFFFFF')
            .opacity(0.8)
            .margin({ top: 8 })
        }
        .width('30%')
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      // 分隔线
      Divider()
        .color('#FFFFFF')
        .opacity(0.3)
        .margin({ left: 16, right: 16, top: 12 })

      // 底部区域:未来天气预报
      Row() {
        ForEach(this.forecast, (item: ForecastData) => {
          this.ForecastItem(item)
        }, (item: ForecastData) => item.day)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
    }
    .width('100%')
    .height('100%')
    .linearGradient({
      direction: GradientDirection.Bottom,
      colors: [['#4FC3F7', 0.0], ['#29B6F6', 1.0]]
    })
    .borderRadius(16)
    .alignItems(HorizontalAlign.Start)
  }
}

六、主页面与卡片管理

6.1 入口页面

创建 ets/pages/Index.ets

import { formProvider } from '@kit.FormKit'
import { abilityDelegatorRegistry } from '@kit.TestKit'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  @State message: string = '天气服务卡片示例'

  build() {
    Column() {
      Text(this.message)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      // 添加2×2卡片按钮
      Button('添加 2×2 天气卡片')
        .width('80%')
        .height(50)
        .fontSize(16)
        .margin({ bottom: 16 })
        .onClick(() => {
          this.addForm('weather_2x2')
        })

      // 添加2×4卡片按钮
      Button('添加 2×4 天气卡片')
        .width('80%')
        .height(50)
        .fontSize(16)
        .margin({ bottom: 16 })
        .onClick(() => {
          this.addForm('weather_2x4')
        })

      // 刷新所有卡片按钮
      Button('刷新所有卡片')
        .width('80%')
        .height(50)
        .fontSize(16)
        .backgroundColor('#4FC3F7')
        .onClick(() => {
          this.refreshAllForms()
        })

      Text('提示:点击按钮添加天气卡片到桌面')
        .fontSize(12)
        .fontColor('#666666')
        .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F5F5F5')
  }

  // 添加表单卡片
  async addForm(formName: string) {
    try {
      const formId = await formProvider.requestForm({
        want: {
          bundleName: 'com.example.weathercard',
          abilityName: 'WeatherFormAbility',
          parameters: {
            'ohos.extra.param.key.form_name': formName
          }
        }
      })
      promptAction.showToast({ message: '卡片已添加到桌面' })
    } catch (err) {
      console.error(`Failed to add form: ${err}`)
      promptAction.showToast({ message: '添加卡片失败' })
    }
  }

  // 刷新所有表单
  async refreshAllForms() {
    try {
      // 获取所有表单ID
      const formIds = await formProvider.getAllFormsInfo()
      for (const formInfo of formIds) {
        await formProvider.updateForm(formInfo.formId, {
          data: JSON.stringify({})
        })
      }
      promptAction.showToast({ message: '卡片已刷新' })
    } catch (err) {
      console.error(`Failed to refresh forms: ${err}`)
      promptAction.showToast({ message: '刷新失败' })
    }
  }
}

七、定时刷新机制

7.1 自动刷新配置

HarmonyOS 提供了两种定时刷新方式:

方式一:固定间隔刷新

form_config.json 中配置 updateDuration(单位:分钟):

{
  "updateDuration": 30
}

方式二:指定时间点刷新

form_config.json 中配置 scheduledUpdateTime

{
  "scheduledUpdateTime": "08:00"
}

7.2 手动刷新实现

在卡片页面中添加刷新按钮:

// 在卡片 UI 中添加刷新按钮
Row() {
  Text(`更新于 ${this.updateTime}`)
    .fontSize(10)
    .fontColor('#FFFFFF')
    .opacity(0.7)

  // 刷新按钮
  Text('🔄')
    .fontSize(14)
    .onClick(() => {
      formBindingData.sendFormMessage(this.formId, 'refresh')
    })
}

7.3 后台定时任务

创建后台服务定期更新天气数据:

// 在 WeatherFormAbility 中添加定时任务
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'

// 启动后台任务
async startBackgroundTask() {
  const wantAgentInfo: backgroundTaskManager.WantAgentInfo = {
    wants: [{
      bundleName: 'com.example.weathercard',
      abilityName: 'WeatherFormAbility'
    }],
    actionType: backgroundTaskManager.OperationType.START_SERVICE,
    requestCode: 0,
    actionFlags: [backgroundTaskManager.WantAgentFlags.UPDATE_PRESENT_FLAG]
  }

  const wantAgent = await wantAgent.getWantAgent(wantAgentInfo)

  const delayTime = 30 * 60 * 1000 // 30分钟

  await backgroundTaskManager.startBackgroundRunning(this.context, {
    title: '天气数据更新',
    wantAgent: wantAgent
  })
}

八、跨设备数据同步

8.1 分布式数据同步

利用分布式 KVStore 实现跨设备数据同步:

import { distributedKVStore } from '@kit.DistributedService'

// 监听数据变化
kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
  console.info(`Data changed: ${JSON.stringify(data)}`)
  // 更新卡片数据
  this.refreshFormData(data)
})

// 跨设备同步数据
async syncDataToOtherDevices(data: WeatherData) {
  if (this.kvStore) {
    await this.kvStore.put(KEY_WEATHER_DATA, JSON.stringify(data))
    // 数据会自动同步到其他设备
  }
}

8.2 设备信息获取

import { deviceManager } from '@kit.DistributedService'

// 获取当前设备信息
const deviceInfos = await deviceManager.getAvailableDeviceListSync()
for (const device of deviceInfos) {
  console.info(`Device: ${device.deviceName}, ID: ${device.networkId}`)
}

九、常见问题与解决方案

9.1 卡片不显示

问题:添加卡片后桌面上没有显示

解决方案

  • 检查 module.json5 中 FormExtensionAbility 配置是否正确

  • 确认 form_config.json 路径和配置是否正确

  • 检查卡片页面路径是否正确

9.2 数据不更新

问题:卡片数据不刷新

解决方案

  • 确认 updateDurationscheduledUpdateTime 已配置

  • 检查网络权限 ohos.permission.INTERNET 是否添加

  • 验证 formProvider.updateForm 是否正确调用

9.3 多尺寸适配问题

问题:不同尺寸卡片显示异常

解决方案

  • 使用百分比布局而非固定尺寸

  • 使用 dimension 参数判断当前卡片尺寸

  • 针对不同尺寸编写独立的 UI 组件

十、总结

通过本教程,你已经掌握了:

  1. 卡片配置form_config.json 的完整配置

  2. 生命周期管理:FormExtensionAbility 的各个回调

  3. 多尺寸适配:2×2 和 2×4 两种尺寸的实现

  4. 数据绑定:使用 formBindingData 传递数据

  5. 定时刷新:自动和手动两种刷新方式

  6. 跨设备同步:利用分布式 KVStore 同步数据

项目源码:完整的天气卡片项目代码已在文中给出,可直接复用到你的项目中。


下一篇预告:我们将开发一个新闻资讯卡片,实现图文混排、滑动列表等高级功能。


系列文章推荐


标签服务卡片 天气卡片 FormKit 鸿蒙开发 万能卡片 ArkUI 跨设备 分布式


💡 提示:如果你在开发过程中遇到问题,欢迎在评论区留言交流!

Logo

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

更多推荐