HarmonyOS 5 Stage模型下ServiceExtensionAbility的使用:天气服务卡片开发
ServiceExtensionAbility是HarmonyOS Stage模型中扩展组件ExtensionAbility的派生类,专门用于提供。它允许应用在无界面的情况下长时间运行,处理后台任务,并为其他组件(如UIAbility或卡片)提供服务支持。在服务卡片场景中,ServiceExtensionAbility(或其特定子类如FormExtensionAbility)扮演着。
🎯 一、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. 跨设备测试
测试跨设备数据同步功能需要:
- 至少两台登录相同华为账号的设备(或模拟器)处于同一局域网内。
- 在两台设备上都安装并运行应用。
- 在一台设备上刷新天气数据,观察另一台设备是否自动更新。
3. 性能优化建议
- 缓存策略:合理设置天气数据的缓存时间,避免频繁网络请求。
- 更新频率:卡片刷新频率建议控制在30秒以内,避免过于频繁的更新影响设备性能。
- 资源释放:在卡片销毁或不可见时,及时释放网络连接、停止定时器等资源。
- 错误处理:网络请求失败时提供友好的降级方案(如显示缓存数据或默认数据)。
💡 八、常见问题与解决方案
- 卡片不更新
- 检查:确认
module.json5
中配置了updateEnabled: true
和scheduledUpdateTime
。 - 检查:确保
onUpdateForm
方法被正确实现且没有抛出异常。
- 检查:确认
- 跨设备数据不同步
- 检查:确认设备已登录相同账号并处于同一局域网。
- 检查:确认已申请
ohos.permission.DISTRIBUTED_DATASYNC
权限。
- 网络请求失败
- 处理:实现良好的错误处理和降级方案,如返回缓存数据或默认数据。
- 检查:确认已申请
ohos.permission.INTERNET
权限。
- 卡片布局错乱
- 处理:使用响应式布局语法,针对不同尺寸的卡片模板进行适配。
通过以上步骤,你可以成功创建一个功能完整、支持跨设备同步的天气服务卡片。ServiceExtensionAbility提供了强大的后台能力,使得卡片能够保持数据更新并与其它设备协同工作,为用户提供一致的无缝体验。
需要参加鸿蒙认证的请点击 鸿蒙认证链接
更多推荐
所有评论(0)