🎯 一、ServiceExtensionAbility 概述

ServiceExtensionAbility是HarmonyOS Stage模型中扩展组件ExtensionAbility的派生类,专门用于提供后台服务能力。它允许应用在无界面的情况下长时间运行,处理后台任务,并为其他组件(如UIAbility或卡片)提供服务支持。

在服务卡片场景中,ServiceExtensionAbility(或其特定子类如FormExtensionAbility)扮演着数据源和逻辑控制中心的角色:

  • 后台数据获取:从网络API(如天气接口)或本地数据库定时拉取和更新数据。
  • 卡片生命周期管理:响应卡片的创建、销毁、更新和可见性变化。
  • 跨设备数据同步:利用HarmonyOS的分布式能力,将数据同步到组网内的其他设备。

⚙️ 二、开发准备与配置

1. 模块配置 (module.json5)

在工程的 module.json5配置文件中,注册ServiceExtensionAbility并声明必要的权限。

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET", // 网络权限,用于获取天气数据
        "reason": "$string:internet_permission_reason",
        "usedScene": {
          "abilities": ["WeatherWidgetExtension"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC", // 分布式数据同步权限
        "reason": "$string:distributed_permission_reason",
        "usedScene": {
          "abilities": ["WeatherWidgetExtension"],
          "when": "always"
        }
      }
    ],
    "extensionAbilities": [
      {
        "name": "WeatherWidgetExtension",
        "srcEntry": "./ets/WeatherWidgetExtension/WeatherWidgetExtension.ets",
        "type": "form", // 类型为form,表示服务卡片
        "exported": true,
        "description": "$string:weather_widget_desc",
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config" // 卡片配置文件
          }
        ]
      }
    ]
  }
}

2. 卡片配置文件 (form_config.json)

resources/base/profile/目录下创建卡片配置文件,定义卡片的基本属性。

{
  "forms": [
    {
      "name": "weather_widget",
      "description": "$string:weather_widget_desc",
      "src": "./ets/WeatherWidgetExtension/WeatherCard/WeatherCard.ets",
      "window": {
        "designWidth": 360,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": 30, // 定时更新间隔(分钟)
      "defaultDimension": "2 * 2",
      "supportDimensions": ["2 * 2", "2 * 4"]
    }
  ]
}

3. 导入模块

在ArkTS文件中导入必要的模块。

import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';
import weather from '@ohos.weather';
import distributedData from '@ohos.data.distributedData';
import { BusinessError } from '@ohos.base';

🧩 三、创建天气服务卡片Ability

创建一个继承自 FormExtensionAbility的类,并重写其生命周期方法。FormExtensionAbility是ServiceExtensionAbility的一种特殊类型,专为服务卡片设计。

// WeatherWidgetExtension.ets
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import formBindingData from '@ohos.app.form.formBindingData';
import { WeatherService } from '../services/WeatherService'; // 假设的天气服务类
import { DistributedService } from '../services/DistributedService'; // 假设的分布式服务类

const TAG: string = 'WeatherWidgetExtension';
const DOMAIN_NUMBER: number = 0xFF00;

export default class WeatherWidgetExtension extends FormExtensionAbility {
  private weatherService: WeatherService = new WeatherService();
  private distributedService: DistributedService = new DistributedService();
  private currentWeather: WeatherData | null = null; // 假设的天气数据类型

  // 当卡片创建时调用
  async onAddForm(want: Want): Promise<formBindingData.FormBindingData> {
    hilog.info(DOMAIN_NUMBER, TAG, 'onAddForm, want:' + want.abilityName);
    
    // 初始化服务
    await this.weatherService.init();
    await this.distributedService.init();

    // 获取初始天气数据
    this.currentWeather = await this.weatherService.fetchWeatherData();
    
    // 创建并返回绑定数据
    return this.createFormBindingData(this.currentWeather);
  }

  // 当卡片需要更新时调用(例如定时更新或手动刷新)
  async onUpdateForm(formId: string): Promise<void> {
    hilog.info(DOMAIN_NUMBER, TAG, `onUpdateForm, formId: ${formId}`);
    
    try {
      // 获取最新的天气数据
      this.currentWeather = await this.weatherService.fetchWeatherData();
      
      // 更新卡片数据
      const bindingData = this.createFormBindingData(this.currentWeather);
      await formProvider.updateForm(formId, bindingData);
      
      hilog.info(DOMAIN_NUMBER, TAG, 'Form updated successfully.');
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to update form: ${(error as BusinessError).message}`);
    }
  }

  // 当卡片销毁时调用
  onRemoveForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onRemoveForm, formId: ${formId}`);
    
    // 清理资源
    this.weatherService.cleanup();
    this.distributedService.cleanup();
  }

  // 当卡片可见性发生变化时调用
  onChangeFormVisibility(newStatus: Record<string, number>): void {
    hilog.info(DOMAIN_NUMBER, TAG, `onChangeFormVisibility, newStatus: ${JSON.stringify(newStatus)}`);
    
    // 可以根据可见性状态决定是否暂停或恢复数据更新,以节省资源
    if (Object.values(newStatus).some(visible => visible === 1)) {
      this.weatherService.resumeUpdates();
    } else {
      this.weatherService.pauseUpdates();
    }
  }

  // 创建表单绑定数据的辅助方法
  private createFormBindingData(weatherData: WeatherData): formBindingData.FormBindingData {
    const formData = {
      city: weatherData.city,
      temperature: `${weatherData.temp}℃`,
      condition: weatherData.condition,
      highTemp: `${weatherData.highTemp}℃`,
      lowTemp: `${weatherData.lowTemp}℃`,
      humidity: `${weatherData.humidity}%`,
      updateTime: this.formatTime(new Date())
    };
    
    return formBindingData.createFormBindingData(formData);
  }

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

📊 四、构建天气数据服务

创建一个独立的天气服务类,负责数据的获取、缓存和更新逻辑。

// WeatherService.ets
import { WeatherAPI } from '../api/WeatherAPI'; // 假设的天气API类
import { CacheManager } from '../utils/CacheManager'; // 假设的缓存管理类

const TAG: string = 'WeatherService';
const DOMAIN_NUMBER: number = 0xFF00;

export class WeatherService {
  private weatherAPI: WeatherAPI;
  private cacheManager: CacheManager;
  private currentLocation: string = 'Beijing'; // 默认位置,可从配置或GPS获取
  private updateInterval: number = 30 * 60 * 1000; // 30分钟更新一次

  async init(): Promise<void> {
    this.weatherAPI = new WeatherAPI();
    this.cacheManager = new CacheManager();
    await this.cacheManager.init();
  }

  async fetchWeatherData(): Promise<WeatherData> {
    try {
      // 尝试从缓存中获取数据
      const cachedData = await this.cacheManager.get<WeatherData>('weather_data');
      if (cachedData && !this.isDataStale(cachedData.timestamp)) {
        hilog.info(DOMAIN_NUMBER, TAG, 'Returning cached weather data.');
        return cachedData;
      }

      // 缓存无效或不存在,从网络获取
      hilog.info(DOMAIN_NUMBER, TAG, 'Fetching fresh weather data from API.');
      const freshData = await this.weatherAPI.getCurrentWeather(this.currentLocation);
      
      // 更新缓存
      freshData.timestamp = Date.now();
      await this.cacheManager.set('weather_data', freshData, this.updateInterval);
      
      return freshData;
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `Error fetching weather data: ${(error as BusinessError).message}`);
      
      // 网络请求失败时,尝试返回过期的缓存数据(优于无数据展示)
      const cachedData = await this.cacheManager.get<WeatherData>('weather_data');
      if (cachedData) {
        hilog.info(DOMAIN_NUMBER, TAG, 'Network failed, returning stale cached data.');
        return cachedData;
      }
      
      // 连缓存也没有,返回默认数据
      return this.getDefaultWeatherData();
    }
  }

  private isDataStale(timestamp: number): boolean {
    return Date.now() - timestamp > this.updateInterval;
  }

  private getDefaultWeatherData(): WeatherData {
    return {
      city: 'Beijing',
      temp: 20,
      condition: 'Cloudy',
      highTemp: 25,
      lowTemp: 15,
      humidity: 60,
      timestamp: Date.now()
    };
  }

  cleanup(): void {
    // 清理资源,如取消网络请求、关闭数据库连接等
    this.weatherAPI.abortRequests();
  }

  resumeUpdates(): void {
    // 恢复数据更新,例如重启定时器
  }

  pauseUpdates(): void {
    // 暂停数据更新,例如停止定时器以节省电量
  }
}

🌐 五、实现跨设备数据同步

利用HarmonyOS的分布式数据管理能力,实现天气数据在多个设备间的自动同步。

// DistributedService.ets
import distributedKVStore from '@ohos.data.distributedKVStore';
import deviceManager from '@ohos.distributedDeviceManager';
import { BusinessError } from '@ohos.base';

const TAG: string = 'DistributedService';
const DOMAIN_NUMBER: number = 0xFF00;
const STORE_ID: string = 'weather_store';
const KEY_WEATHER_DATA: string = 'current_weather';

export class DistributedService {
  private kvManager: distributedKVStore.KVManager | null = null;
  private kvStore: distributedKVStore.SingleKVStore | null = null;

  async init(): Promise<void> {
    try {
      // 初始化KVManager
      const context = getContext(this) as Context;
      const kvManagerConfig: distributedKVStore.Config = {
        bundleName: context.applicationInfo.name,
        userInfo: {
          userId: '0', // 同一用户ID下的设备可以同步数据
          userType: distributedKVStore.UserType.SAME_USER_ID
        }
      };
      
      this.kvManager = distributedKVStore.createKVManager(kvManagerConfig);
      
      // 获取或创建KVStore
      const options: distributedKVStore.StoreConfig = {
        storeId: STORE_ID,
        kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
        securityLevel: distributedKVStore.SecurityLevel.S2,
        autoSync: true, // 开启自动同步
        encrypt: true // 加密存储
      };
      
      this.kvStore = await this.kvManager.getKVStore<distributedKVStore.SingleKVStore>(options);
      
      // 订阅数据变更事件
      await this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
        this.onDataChanged(data);
      });
      
      hilog.info(DOMAIN_NUMBER, TAG, 'Distributed service initialized successfully.');
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to init distributed service: ${(error as BusinessError).message}`);
    }
  }

  // 同步数据到其他设备
  async syncWeatherData(weatherData: WeatherData): Promise<void> {
    if (!this.kvStore) {
      hilog.error(DOMAIN_NUMBER, TAG, 'KVStore is not initialized.');
      return;
    }
    
    try {
      const weatherJson = JSON.stringify(weatherData);
      await this.kvStore.put(KEY_WEATHER_DATA, weatherJson);
      hilog.info(DOMAIN_NUMBER, TAG, 'Weather data synced to distributed store.');
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to sync weather data: ${(error as BusinessError).message}`);
    }
  }

  // 从其他设备获取数据
  async getSyncedWeatherData(): Promise<WeatherData | null> {
    if (!this.kvStore) {
      return null;
    }
    
    try {
      const entries = await this.kvStore.getEntries(KEY_WEATHER_DATA);
      if (entries.length > 0) {
        const weatherJson = entries[0].value.value as string;
        return JSON.parse(weatherJson) as WeatherData;
      }
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to get synced weather data: ${(error as BusinessError).message}`);
    }
    
    return null;
  }

  // 处理数据变更事件
  private onDataChanged(data: distributedKVStore.ChangeData): void {
    if (data.key === KEY_WEATHER_DATA) {
      hilog.info(DOMAIN_NUMBER, TAG, `Weather data changed on remote device: ${data.value}`);
      
      // 通知UI更新
      // 可以通过Emitter或其他方式通知WeatherWidgetExtension更新卡片
    }
  }

  cleanup(): void {
    if (this.kvStore) {
      this.kvStore.off('dataChange', () => {});
    }
    this.kvStore = null;
    this.kvManager = null;
  }
}

🖥️ 六、构建卡片UI组件

创建卡片的UI组件,展示天气信息并处理用户交互。

// WeatherCard.ets
@Component
export struct WeatherCard {
  @Prop city: string = '--';
  @Prop temperature: string = '--';
  @Prop condition: string = '--';
  @Prop highTemp: string = '--';
  @Prop lowTemp: '--';
  @Prop humidity: string = '--';
  @Prop updateTime: string = '--';
  
  @State isRefreshing: boolean = false;

  // 刷新按钮点击事件
  private async onRefresh(): Promise<void> {
    this.isRefreshing = true;
    
    // 通过postCardAction触发卡片的onUpdateForm方法
    postCardAction(this, {
      action: 'refresh',
      params: {}
    });
    
    // 模拟网络延迟,2秒后停止刷新动画
    setTimeout(() => {
      this.isRefreshing = false;
    }, 2000);
  }

  build() {
    Column() {
      // 城市和更新时间
      Row() {
        Text(this.city)
          .fontSize(16)
          .fontColor(Color.White)
        
        Text(`更新: ${this.updateTime}`)
          .fontSize(12)
          .fontColor(Color.White)
          .opacity(0.8)
          .margin({ left: 8 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 温度和天气状况
      Row() {
        Text(this.temperature)
          .fontSize(32)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
        
        Column() {
          Text(this.condition)
            .fontSize(14)
            .fontColor(Color.White)
          
          Text(`H:${this.highTemp} L:${this.lowTemp}`)
            .fontSize(12)
            .fontColor(Color.White)
            .opacity(0.8)
        }
        .margin({ left: 12 })
      }
      .width('100%')
      .margin({ top: 8 })

      // 湿度信息和刷新按钮
      Row() {
        Text(`湿度 ${this.humidity}`)
          .fontSize(12)
          .fontColor(Color.White)
          .opacity(0.8)
        
        LoadingIndicator()
          .size({ width: 16, height: 16 })
          .color(Color.White)
          .visibility(this.isRefreshing ? Visibility.Visible : Visibility.None)
        
        Image($r('app.media.ic_refresh'))
          .width(16)
          .height(16)
          .onClick(() => this.onRefresh())
          .visibility(this.isRefreshing ? Visibility.None : Visibility.Visible)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 12 })
    }
    .padding(16)
    .backgroundColor(this.getBackgroundColor())
    .borderRadius(12)
  }

  // 根据天气状况返回不同的背景色
  private getBackgroundColor(): ResourceColor {
    switch (this.condition) {
      case 'Sunny':
        return '#FFA500';
      case 'Cloudy':
        return '#708090';
      case 'Rainy':
        return '#4682B4';
      case 'Snowy':
        return '#87CEEB';
      default:
        return '#4A5568';
    }
  }
}

🔧 七、测试与调试

1. 本地测试

在DevEco Studio中,可以通过以下方式测试服务卡片:

  • 使用预览器(Previewer)快速查看UI效果。
  • 在模拟器或真机上运行,测试完整的后台数据获取和卡片更新流程。
  • 使用HiLog查看日志,调试数据获取和同步逻辑。

2. 跨设备测试

测试跨设备数据同步功能需要:

  1. 至少两台登录相同华为账号的设备(或模拟器)处于同一局域网内。
  2. 在两台设备上都安装并运行应用。
  3. 在一台设备上刷新天气数据,观察另一台设备是否自动更新。

3. 性能优化建议

  • 缓存策略:合理设置天气数据的缓存时间,避免频繁网络请求。
  • 更新频率:卡片刷新频率建议控制在30秒以内,避免过于频繁的更新影响设备性能。
  • 资源释放:在卡片销毁或不可见时,及时释放网络连接、停止定时器等资源。
  • 错误处理:网络请求失败时提供友好的降级方案(如显示缓存数据或默认数据)。

💡 八、常见问题与解决方案

  1. 卡片不更新
    • 检查:确认 module.json5中配置了 updateEnabled: truescheduledUpdateTime
    • 检查:确保 onUpdateForm方法被正确实现且没有抛出异常。
  2. 跨设备数据不同步
    • 检查:确认设备已登录相同账号并处于同一局域网。
    • 检查:确认已申请 ohos.permission.DISTRIBUTED_DATASYNC权限。
  3. 网络请求失败
    • 处理:实现良好的错误处理和降级方案,如返回缓存数据或默认数据。
    • 检查:确认已申请 ohos.permission.INTERNET权限。
  4. 卡片布局错乱
    • 处理:使用响应式布局语法,针对不同尺寸的卡片模板进行适配。

通过以上步骤,你可以成功创建一个功能完整、支持跨设备同步的天气服务卡片。ServiceExtensionAbility提供了强大的后台能力,使得卡片能够保持数据更新并与其它设备协同工作,为用户提供一致的无缝体验。

需要参加鸿蒙认证的请点击 鸿蒙认证链接

Logo

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

更多推荐