天气 App(下):通知提醒与设置页
系列: HarmonyOS 本地工具 App 实战 · 第 2 篇
上一篇: HarmonyOS 天气 App 实战(上):项目搭建与天气主页
注意:本文介绍的 Notification Kit 通知推送、Settings 设置页、城市切换 Swiper 轮播、animateTo 页面动画等功能属于设计模式与参考实现,是对 Part 1 已完成的天气主页的扩展方案。当前项目聚焦于 Part 1 实现的核心天气展示功能(QWeather API 天气查询、7 天预报、定位获取),本文中的代码示例和架构模式均为可落地的扩展方案,尚未集成到当前工程中。读者可参照本文逐步将这些功能接入自己的项目。
引言
上一篇我们完成了天气 App 的主页搭建,能够实时显示当前天气和 7 天预报。但一个好用的天气应用还需要更多功能:当气象台发布暴雨、高温预警时,用户能否第一时间收到通知?切换城市查看不同地区的天气是否方便?温度单位能否在摄氏度和华氏度之间自由切换?
本文将为天气 App 补齐这些关键功能。我们将使用 Notification Kit 实现天气预警通知推送,使用 Preferences 持久化用户设置,通过 animateTo 实现页面切换动画,并用 Swiper 和 Toggle/Select 组件打造流畅的交互体验。
通过本文你将学到:
- 如何使用 Notification Kit 发布本地通知并创建通知渠道
- 如何使用 Preferences 持久化存储用户设置(温度单位、通知开关等)
- 如何使用 animateTo 实现组件的平滑过渡动画
- 如何使用 Swiper 实现城市卡片轮播切换
- 如何使用 Toggle 和 Select 组件构建设置页面

实现边界:当前工程已完成 QWeather API 天气查询与 7 天预报展示,在真机上可正常获取实时天气数据。本篇描述的通知渠道(Notification Kit)、设置页(Preferences 持久化)、城市轮播(Swiper)和页面动画(animateTo)均为扩展模式,属于设计参考实现,尚未集成到当前工程。读者可在天气主页功能稳定后,参照本文逐步接入这些扩展功能。
环境准备
| 项目 | 版本 |
|---|---|
| DevEco Studio | 6.1.1 Release |
| API Level | API 24 |
| 设备 | 真机或模拟器 |
依赖配置
本篇新增 Notification Kit 和 ArkData(Preferences)两个系统 Kit,均通过 @kit.* 导入,无需额外安装:
// oh-package.json5
{
"name": "weather_app",
"version": "1.0.0",
"description": "天气查询应用",
"main": "",
"author": "",
"license": "Apache-2.0",
"dependencies": {},
"devDependencies": {}
}
权限配置
天气预警通知需要通知权限。从 API 23 开始,应用发布本地通知前需要先创建通知渠道(NotificationSlot),但无需申请额外权限:
// module.json5
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "获取当前位置以查询当地天气",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
说明:Notification Kit 发布本地通知不需要特殊权限,但需要先通过
addSlot创建通知渠道。系统会自动向用户展示通知授权弹窗。
核心实现
Step 1: 定义设置模型与 Preferences 持久化
目标: 使用 @ObservedV2 + @Trace 定义应用设置模型,并通过 Preferences 实现设置数据的持久化存储。
在上一篇的基础上,我们新增一个 SettingsModel 类来管理所有用户设置:
// model/SettingsModel.ets
import { AppStorageV2 } from '@kit.ArkUI';
/** 温度单位 */
export enum TemperatureUnit {
CELSIUS = 'celsius',
FAHRENHEIT = 'fahrenheit'
}
/** 应用设置模型 */
@ObservedV2
export class SettingsModel {
@Trace temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS;
@Trace notificationEnabled: boolean = true;
@Trace autoRefresh: boolean = true;
@Trace currentCityIndex: number = 0;
@Trace isDarkMode: boolean = false;
}
/** 获取全局设置单例 */
export function getSettings(): SettingsModel {
return AppStorageV2.connect(SettingsModel, 'appSettings', () => new SettingsModel())!;
}
@ObservedV2 和 @Trace 是 HarmonyOS 状态管理 V2 的核心装饰器。与 V1 的 @State 不同,@Trace 能够深度监听对象属性的变化——即使属性是嵌套对象或数组,修改其内部成员也能触发 UI 刷新。
接下来实现 Preferences 持久化服务:
// service/SettingsService.ets
import { dataPreferences } from '@kit.ArkData';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { SettingsModel, TemperatureUnit } from '../model/SettingsModel';
const TAG = 'SettingsService';
const PREFS_NAME = 'weather_settings';
/** 设置持久化服务 */
export class SettingsService {
private prefs: dataPreferences.Preferences | undefined = undefined;
/** 初始化 Preferences 实例 */
async init(context: common.UIAbilityContext): Promise<void> {
this.prefs = await dataPreferences.getPreferences(context, {
name: PREFS_NAME
});
hilog.info(0x0000, TAG, 'Preferences 初始化成功');
}
/** 从磁盘加载设置到模型 */
async loadSettings(settings: SettingsModel): Promise<void> {
if (!this.prefs) {
return;
}
settings.temperatureUnit = (await this.prefs.get(
'temperatureUnit', TemperatureUnit.CELSIUS
)) as TemperatureUnit;
settings.notificationEnabled = (await this.prefs.get(
'notificationEnabled', true
)) as boolean;
settings.autoRefresh = (await this.prefs.get(
'autoRefresh', true
)) as boolean;
settings.currentCityIndex = (await this.prefs.get(
'currentCityIndex', 0
)) as number;
settings.isDarkMode = (await this.prefs.get(
'isDarkMode', false
)) as boolean;
}
/** 保存单个设置项 */
async saveSetting(key: string, value: string | number | boolean): Promise<void> {
if (!this.prefs) {
return;
}
await this.prefs.put(key, value);
await this.prefs.flush();
hilog.info(0x0000, TAG, `设置已保存: ${key} = ${value}`);
}
}
关键点说明:
getPreferences是异步方法,返回的Preferences实例会自动在内存中缓存数据,后续的get操作直接读取缓存,性能很高。- 每次
put后调用flush将数据写入磁盘,确保应用异常退出时不丢失设置。 Preferences支持number、string、boolean等基本类型,不适合存储大量数据。
Step 2: 实现设置页面
目标: 使用 Toggle 和 Select 组件构建设置页面,支持温度单位切换、通知开关、自动刷新等功能。
// pages/SettingsPage.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { SettingsModel, TemperatureUnit, getSettings } from '../model/SettingsModel';
import { SettingsService } from '../service/SettingsService';
@Entry
@Component
struct SettingsPage {
@ObjectLink settings: SettingsModel = getSettings();
private settingsService: SettingsService = new SettingsService();
aboutToAppear(): void {
this.settingsService.init(getContext(this));
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('设置')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 12 })
// 设置列表
List({ space: 0 }) {
// 温度单位选择
ListItem() {
this.SettingItemBuilder(
'温度单位',
this.settings.temperatureUnit === TemperatureUnit.CELSIUS ? '摄氏度 (°C)' : '华氏度 (°F)',
'select'
)
}
// 天气通知开关
ListItem() {
this.SettingItemBuilder(
'天气预警通知',
'',
'toggle',
this.settings.notificationEnabled,
async (value: boolean) => {
this.settings.notificationEnabled = value;
await this.settingsService.saveSetting('notificationEnabled', value);
}
)
}
// 自动刷新开关
ListItem() {
this.SettingItemBuilder(
'自动刷新天气',
'',
'toggle',
this.settings.autoRefresh,
async (value: boolean) => {
this.settings.autoRefresh = value;
await this.settingsService.saveSetting('autoRefresh', value);
}
)
}
// 深色模式开关
ListItem() {
this.SettingItemBuilder(
'深色模式',
'',
'toggle',
this.settings.isDarkMode,
async (value: boolean) => {
this.settings.isDarkMode = value;
await this.settingsService.saveSetting('isDarkMode', value);
}
)
}
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/** 通用设置项构建器 */
@Builder
SettingItemBuilder(
title: string,
subtitle: string,
controlType: string,
toggleValue?: boolean,
onToggleChange?: (value: boolean) => void
) {
Row() {
// 左侧文字
Column({ space: 4 }) {
Text(title)
.fontSize(16)
.fontColor('#333333')
if (subtitle) {
Text(subtitle)
.fontSize(13)
.fontColor('#999999')
}
}
.alignItems(HorizontalAlign.Start)
Blank()
// 右侧控件
if (controlType === 'select') {
Select([
{ value: '摄氏度 (°C)', icon: '' },
{ value: '华氏度 (°F)', icon: '' }
])
.selected(this.settings.temperatureUnit === TemperatureUnit.CELSIUS ? 0 : 1)
.value(this.settings.temperatureUnit === TemperatureUnit.CELSIUS ? '摄氏度' : '华氏度')
.onSelect(async (index: number) => {
const unit = index === 0
? TemperatureUnit.CELSIUS
: TemperatureUnit.FAHRENHEIT;
this.settings.temperatureUnit = unit;
await this.settingsService.saveSetting('temperatureUnit', unit);
})
.width(120)
} else if (controlType === 'toggle' && toggleValue !== undefined) {
Toggle({ type: ToggleType.Switch, isOn: toggleValue })
.onChange(async (isOn: boolean) => {
if (onToggleChange) {
onToggleChange(isOn);
}
})
.selectedColor('#4A90D9')
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 1 })
}
}
实现效果:设置页面包含温度单位选择(Select 下拉框)、天气预警通知开关(Toggle)、自动刷新开关、深色模式开关四个设置项,每项左侧为标题和副标题,右侧为对应控件。
这段代码有几个设计要点:
@ObjectLink 装饰器:配合 @ObservedV2 使用,用于接收从父组件传入的可观察对象。与 @State 不同,@ObjectLink 不拥有对象本身,而是引用父组件传递的同一个实例。当 SettingsModel 的任何 @Trace 属性变化时,使用 @ObjectLink 接收的组件会自动刷新。
Select 组件:用于温度单位选择。selected 属性绑定当前选中项索引,onSelect 回调在用户选择新选项时触发。注意 value 属性设置的是 Select 控件上显示的文字。
Toggle 组件:开关控件,type: ToggleType.Switch 表示滑动开关样式。isOn 绑定开关状态,onChange 在用户切换时触发。selectedColor 设置开启时的轨道颜色。
Step 3: 温度单位转换
目标: 在天气主页根据用户设置的温度单位自动转捾示温度值。
// service/TempConverter.ets
import { TemperatureUnit } from '../model/SettingsModel';
/** 温度转换工具 */
export class TempConverter {
/**
* 格式化温度显示
* @param celsius 摄氏度温度值
* @param unit 目标单位
* @returns 格式化后的温度字符串
*/
static format(celsius: number, unit: TemperatureUnit): string {
if (unit === TemperatureUnit.FAHRENHEIT) {
const fahrenheit = Math.round(celsius * 9 / 5 + 32);
return `${fahrenheit}°F`;
}
return `${celsius}°C`;
}
/**
* 转换温度数值
* @param celsius 摄氏度
* @param unit 目标单位
* @returns 转换后的温度数值
*/
static convert(celsius: number, unit: TemperatureUnit): number {
if (unit === TemperatureUnit.FAHRENHEIT) {
return Math.round(celsius * 9 / 5 + 32);
}
return celsius;
}
}
在天气主页中引入温度转换:
// pages/Index.ets(部分修改)
import { getSettings, TemperatureUnit } from '../model/SettingsModel';
import { TempConverter } from '../service/TempConverter';
@Entry
@Component
struct WeatherPage {
@ObjectLink settings: SettingsModel = getSettings();
// ... 其他状态变量 ...
build() {
Column() {
// ... 顶部导航栏 ...
// 当前温度(根据单位自动转换)
Text(TempConverter.format(this.currentCelsius, this.settings.temperatureUnit))
.fontSize(96)
.fontWeight(FontWeight.Light)
.fontColor(Color.White)
// 预报列表中的温度也需要转换
ForEach(this.forecasts, (day: DailyForecast) => {
Row() {
Text(TempConverter.format(day.minTemperature, this.settings.temperatureUnit))
.fontSize(14)
.fontColor('#B0FFFFFF')
// 温度条 ...
Text(TempConverter.format(day.maxTemperature, this.settings.temperatureUnit))
.fontSize(14)
.fontColor(Color.White)
}
})
}
}
}
由于 settings.temperatureUnit 使用了 @Trace 装饰,当用户在设置页面切换单位后返回主页,温度显示会自动更新,无需手动刷新。
实现效果:Select 组件展开后显示摄氏度 (°C) 和华氏度 (°F) 两个选项,选中项高亮显示,切换后主页温度自动更新。
Step 4: 天气预警通知
目标: 使用 Notification Kit 创建通知渠道,在检测到天气预警时推送本地通知。
// service/NotificationService.ets
import { notificationManager } from '@kit.NotificationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'NotificationService';
/** 天气预警等级 */
export enum AlertLevel {
BLUE = '蓝色',
YELLOW = '黄色',
ORANGE = '橙色',
RED = '红色'
}
/** 预警信息 */
export interface WeatherAlert {
id: number;
title: string;
content: string;
level: AlertLevel;
publishTime: string;
}
/** 通知服务 */
export class NotificationService {
private channelCreated: boolean = false;
/** 创建天气预警通知渠道 */
async createAlertChannel(): Promise<void> {
if (this.channelCreated) {
return;
}
const slot: notificationManager.NotificationSlot = {
type: notificationManager.SlotType.SOCIAL_COMMUNICATION,
level: notificationManager.SlotLevel.LEVEL_HIGH,
desc: '接收天气预警信息,包括暴雨、高温、大风等气象预警',
name: '天气预警',
vibrationEnabled: true,
soundEnabled: true,
lightEnabled: true,
lightColor: 0xFFFF0000
};
try {
await notificationManager.addSlot(slot);
this.channelCreated = true;
hilog.info(0x0000, TAG, '天气预警通知渠道创建成功');
} catch (err) {
hilog.error(0x0000, TAG, `创建通知渠道失败: ${JSON.stringify(err)}`);
}
}
/** 发布天气预警通知 */
async publishAlert(alert: WeatherAlert): Promise<void> {
await this.createAlertChannel();
const request: notificationManager.NotificationRequest = {
id: alert.id,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: `${alert.level}预警:${alert.title}`,
text: alert.content
}
},
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
tapDismissed: true
};
try {
await notificationManager.publish(request);
hilog.info(0x0000, TAG, `预警通知发布成功: ${alert.title}`);
} catch (err) {
hilog.error(0x0000, TAG, `预警通知发布失败: ${JSON.stringify(err)}`);
}
}
/** 取消指定预警通知 */
async cancelAlert(alertId: number): Promise<void> {
try {
await notificationManager.cancel(alertId);
} catch (err) {
hilog.error(0x0000, TAG, `取消通知失败: ${JSON.stringify(err)}`);
}
}
}
实现效果:首次发布通知时,系统弹出通知授权弹窗,用户授权后状态栏显示天气预警通知卡片。
在天气服务中集成预警检查逻辑:
// service/WeatherService.ets(新增方法)
import { NotificationService, WeatherAlert, AlertLevel } from './NotificationService';
export class WeatherService {
private notificationService: NotificationService = new NotificationService();
/** 检查并推送天气预警 */
async checkAndNotifyAlerts(weatherData: WeatherData): Promise<void> {
const alerts: WeatherAlert[] = [];
// 解析 Weather Service Kit 返回的预警数据
if (weatherData.weatherAlerts && weatherData.weatherAlerts.length > 0) {
for (const rawAlert of weatherData.weatherAlerts) {
const alert: WeatherAlert = {
id: rawAlert.alertId ?? Date.now(),
title: rawAlert.title ?? '天气预警',
content: rawAlert.detail ?? '请关注最新天气变化',
level: this.parseAlertLevel(rawAlert.level),
publishTime: rawAlert.publishTime ?? ''
};
alerts.push(alert);
}
}
// 逐条推送预警通知
for (const alert of alerts) {
await this.notificationService.publishAlert(alert);
}
}
/** 解析预警等级 */
private parseAlertLevel(level: string | undefined): AlertLevel {
switch (level) {
case 'red':
return AlertLevel.RED;
case 'orange':
return AlertLevel.ORANGE;
case 'yellow':
return AlertLevel.YELLOW;
default:
return AlertLevel.BLUE;
}
}
}
Notification Kit 的几个关键点:
- 通知渠道(NotificationSlot):Android 8.0 引入的通知分类机制,HarmonyOS 同样采用。不同渠道可以设置不同的提醒方式(振动、声音、灯光)。
LEVEL_HIGH表示高优先级,会在状态栏显示 Heads-up 通知。 - 通知 ID:相同 ID 的通知会覆盖前一条,不同 ID 的通知独立显示。预警通知建议使用预警类型作为 ID,避免重复推送。
tapDismissed: true:用户点击通知后自动清除,适合一次性的预警信息。
Step 5: 城市切换页面
目标: 使用 Swiper 组件实现城市卡片轮播,支持添加和管理多个城市。
// model/CityData.ets
/** 城市天气摘要 */
export interface CityWeather {
cityName: string;
temperature: number;
weatherDescription: string;
isCurrentLocation: boolean;
}
/** 城市列表管理 */
@ObservedV2
export class CityListManager {
@Trace cities: CityWeather[] = [];
@Trace selectedIndex: number = 0;
addCity(city: CityWeather): void {
this.cities = [...this.cities, city];
}
removeCity(index: number): void {
const updated = this.cities.filter((_: CityWeather, i: number) => i !== index);
this.cities = updated;
if (this.selectedIndex >= this.cities.length) {
this.selectedIndex = Math.max(0, this.cities.length - 1);
}
}
}
城市切换页面:
// pages/CityPage.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { CityWeather, CityListManager } from '../model/CityData';
import { TempConverter } from '../service/TempConverter';
import { getSettings, TemperatureUnit } from '../model/SettingsModel';
@Entry
@Component
struct CityPage {
@ObjectLink cityManager: CityListManager;
@ObjectLink settings: SettingsModel = getSettings();
@State showAddCity: boolean = false;
@State searchKeyword: string = '';
build() {
Column() {
// 顶部标题
Row() {
Text('城市管理')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Text('+ 添加城市')
.fontSize(14)
.fontColor('#4A90D9')
.onClick(() => {
this.showAddCity = true;
})
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 12 })
// 城市卡片轮播
Swiper() {
ForEach(this.cityManager.cities, (city: CityWeather, index: number) => {
Column({ space: 12 }) {
// 城市名称
Row() {
Text(city.cityName)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
if (city.isCurrentLocation) {
Text(' 定位')
.fontSize(12)
.fontColor('#B0FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#30FFFFFF')
.borderRadius(8)
}
}
// 天气描述
Text(city.weatherDescription)
.fontSize(14)
.fontColor('#D0FFFFFF')
// 温度
Text(TempConverter.format(city.temperature, this.settings.temperatureUnit))
.fontSize(64)
.fontWeight(FontWeight.Light)
.fontColor(Color.White)
}
.width('100%')
.height(200)
.padding(24)
.backgroundColor('#4A90D9')
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
})
}
.index(this.cityManager.selectedIndex)
.autoPlay(false)
.indicator(Indicator.dot()
.color('#CCCCCC')
.selectedColor('#4A90D9'))
.margin({ left: 16, right: 16 })
.onChange((index: number) => {
this.cityManager.selectedIndex = index;
})
// 城市列表
List({ space: 8 }) {
ForEach(this.cityManager.cities, (city: CityWeather, index: number) => {
ListItem() {
Row() {
Column({ space: 4 }) {
Text(city.cityName)
.fontSize(16)
.fontColor('#333333')
Text(city.weatherDescription)
.fontSize(13)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(TempConverter.format(city.temperature, this.settings.temperatureUnit))
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
// 删除按钮(定位城市不可删除)
if (!city.isCurrentLocation) {
Text('×')
.fontSize(20)
.fontColor('#CCCCCC')
.margin({ left: 12 })
.onClick(() => {
this.cityManager.removeCity(index);
})
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
实现效果:城市切换页面顶部为 Swiper 轮播区域,展示当前城市天气卡片(蓝色背景、城市名、天气描述、温度),底部圆点指示器高亮当前选中城市;下方列表显示所有已添加城市,支持滑动删除。
Swiper 组件的核心属性:
index:当前显示的页面索引,支持双向绑定autoPlay:是否自动轮播,城市切换场景建议关闭indicator:底部指示器样式,dot为圆点样式onChange:页面切换时的回调,参数为新的页面索引
Step 6: 页面切换动画
目标: 使用 animateTo 实现从主页到设置页面的平滑过渡效果。
// pages/Index.ets(新增动画相关代码)
@Entry
@Component
struct WeatherPage {
// ... 其他状态 ...
@State showSettings: boolean = false;
@State settingsOffsetX: number = 100;
@State settingsOpacity: number = 0;
build() {
Stack() {
// 天气主页内容
Column() {
// ... 主页内容 ...
}
.width('100%')
.height('100%')
// 设置页面(覆盖层)
if (this.showSettings) {
SettingsPage()
.width('100%')
.height('100%')
.translate({ x: `${this.settingsOffsetX}%` })
.opacity(this.settingsOpacity)
}
}
.width('100%')
.height('100%')
}
/** 打开设置页面(带动画) */
openSettings(): void {
this.showSettings = true;
animateTo({
duration: 300,
curve: Curve.EaseOut,
onFinish: () => {
hilog.info(0x0000, 'Anim', '设置页面打开动画完成');
}
}, () => {
this.settingsOffsetX = 0;
this.settingsOpacity = 1;
});
}
/** 关闭设置页面(带动画) */
closeSettings(): void {
animateTo({
duration: 250,
curve: Curve.EaseIn,
onFinish: () => {
this.showSettings = false;
}
}, () => {
this.settingsOffsetX = 100;
this.settingsOpacity = 0;
});
}
}
实现效果:点击设置入口后,设置页面从右侧滑入(translate + opacity 动画),300ms EaseOut 曲线;关闭时反向滑出,250ms EaseIn 曲线。
animateTo 是 ArkUI 提供的显式动画 API,核心参数:
duration:动画时长,毫秒为单位。打开页面用 300ms 稍慢以突出效果,关闭用 250ms 稍快以提升响应感。curve:动画曲线。EaseOut表示先快后慢,适合进入动画;EaseIn表示先慢后快,适合退出动画。onFinish:动画结束回调。关闭动画结束后隐藏组件,避免组件仍占据布局空间。- 闭包内的属性变化会被框架自动捕获并生成过渡动画,无需手动计算关键帧。
Step 7: 下拉刷新
目标: 为天气主页添加下拉刷新功能,提升用户体验。
// pages/Index.ets(下拉刷新相关代码)
@Entry
@Component
struct WeatherPage {
@State isRefreshing: boolean = false;
@State refreshProgress: number = 0;
build() {
Column() {
// 顶部导航栏
this.TopBarBuilder()
// 可下拉刷新区域
Refresh({ refreshing: $$this.isRefreshing }) {
// 天气内容
if (this.isLoading) {
this.LoadingBuilder()
} else if (this.errorMsg) {
this.ErrorBuilder()
} else {
this.WeatherContentBuilder()
}
}
.onRefreshing(() => {
// 触发刷新
this.refreshWeatherData();
})
.refreshOffset(64)
.pullToRefresh(true)
}
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#4A90D9', 0.0], ['#74B9FF', 0.5], ['#A8D8EA', 1.0]]
})
}
/** 刷新天气数据 */
async refreshWeatherData(): Promise<void> {
this.isRefreshing = true;
try {
await this.loadWeatherData();
} finally {
this.isRefreshing = false;
}
}
}
实现效果:用户在天气主页下拉时,顶部出现刷新指示器(旋转加载图标),松手后自动请求最新天气数据,完成后指示器消失。
Refresh 组件的关键点:
$$this.isRefreshing:使用双向绑定语法$$将组件的 refreshing 状态与变量同步。onRefreshing:下拉触发刷新时的回调,在此执行数据加载逻辑。refreshOffset:触发刷新的下拉距离阈值,单位 vp。pullToRefresh(true):启用下拉刷新手势。
关键代码解读
Preferences 与 AppStorageV2 联动
本篇的核心设计是将 Preferences 的持久化能力与 AppStorageV2 的响应式能力结合:
// 连接全局设置
@ObjectLink settings: SettingsModel = getSettings();
AppStorageV2.connect 确保整个应用共享同一个 SettingsModel 实例。任何页面修改设置后,其他页面通过 @ObjectLink 引用同一实例的组件会自动刷新。这比传统的"Preferences 读取 → @State 赋值"模式更简洁,也避免了多页面数据不同步的问题。
数据流向:
用户操作 → @Trace 属性变化 → UI 自动刷新
↓
SettingsService.saveSetting()
↓
Preferences.flush() → 磁盘持久化
反向加载:
应用启动 → SettingsService.init() → Preferences 加载到内存
↓
SettingsService.loadSettings()
↓
@Trace 属性赋值 → UI 自动刷新
animateTo 动画设计原则
animateTo({
duration: 300,
curve: Curve.EaseOut,
onFinish: () => { /* 动画结束回调 */ }
}, () => {
// 在闭包中修改的属性会被自动捕获为动画目标
this.settingsOffsetX = 0;
this.settingsOpacity = 1;
});
animateTo 的设计遵循"声明目标状态"的理念:你只需要告诉框架最终状态是什么,框架会自动计算中间帧。这与 CSS transition 的思路一致,但更强大:
- 支持同时动画多个属性(位移 + 透明度)
- 支持动画曲线(
Curve枚举提供多种预设曲线) - 支持动画完成回调(
onFinish),可以在动画结束后执行逻辑
注意 onFinish 的使用时机:关闭动画结束后才设置 showSettings = false,如果在动画开始前就隐藏组件,用户会看到组件突然消失而不是滑出消失。
通知渠道最佳实践
const slot: notificationManager.NotificationSlot = {
type: notificationManager.SlotType.SOCIAL_COMMUNICATION,
level: notificationManager.SlotLevel.LEVEL_HIGH,
vibrationEnabled: true,
soundEnabled: true
};
通知渠道的 level 决定了通知的打扰程度:
| Level | 行为 | 适用场景 |
|---|---|---|
LEVEL_NONE |
静默通知,不振动不响铃 | 后台数据同步 |
LEVEL_LOW |
仅在状态栏显示 | 普通信息推送 |
LEVEL_DEFAULT |
默认行为 | 一般应用通知 |
LEVEL_HIGH |
Heads-up 弹出显示 | 天气预警、紧急通知 |
天气预警属于高优先级信息,使用 LEVEL_HIGH 确保用户能及时看到。但不要滥用高优先级,否则用户会关闭通知权限。
总结
本文为天气 App 补齐了通知和设置功能,主要实现了:
- 设置模型与持久化:使用
@ObservedV2+@Trace定义响应式设置模型,通过 Preferences 实现设置数据的磁盘持久化 - 设置页面 UI:使用 Select 组件实现温度单位选择,Toggle 组件实现开关类设置
- 温度单位转换:封装 TempConverter 工具类,在所有温度显示处统一处理单位转换
- 天气预警通知:使用 Notification Kit 创建高优先级通知渠道,在检测到气象预警时推送本地通知
- 城市切换:使用 Swiper 实现城市卡片轮播,支持添加和删除城市
- 页面切换动画:使用
animateTo实现设置页面的滑入/滑出过渡效果 - 下拉刷新:使用 Refresh 组件实现天气数据的手动刷新
关键注意事项:
Preferences的put操作仅修改内存缓存,必须调用flush才能持久化到磁盘- 通知渠道必须在发布通知前创建,且同一渠道重复创建不会报错
animateTo闭包内的属性变化才会被动画化,闭包外的变化是即时的@ObjectLink必须配合@ObservedV2类使用,不能用于普通类
下篇预告
下一篇: 待办 App(上):任务列表与数据持久化
第三篇我们将开始全新的待办 App 项目,学习如何使用 relationalStore 实现本地数据持久化,以及使用 List + LazyForEach 高效渲染长列表。
更多推荐


所有评论(0)