HarmonyOS 6学习:天气服务集成与智能分享的完整解决方案
通过这次天气应用的完整开发,我总结了几个关键经验:灵活应对服务限制:当官方服务不可用时,第三方API是可行的替代方案,但要考虑数据稳定性和成本。数据降级策略很重要:对于可能为空的数据字段,一定要设计降级方案,确保用户体验的连续性。分享功能要兼顾美观和性能:动态生成海报虽然灵活,但性能消耗大;组件截图方案在性能和效果之间取得了更好的平衡。用户体验细节决定成败:自适应背景色、智能文案生成、优雅的空状态
从"服务不可用"到"一键分享":一次完整的天气应用开发实战
在HarmonyOS 6应用开发中,我最近接到了一个天气应用开发需求:用户想要一个能够显示实时天气、空气质量、未来预报,并且能一键分享天气信息给朋友的应用。听起来很简单,对吧?但实际开发中,我遇到了两个让人头疼的问题。
第一个问题来自华为官方文档的"温馨提示":Weather Service Kit当前仅面向系统应用开放。这意味着普通开发者根本无法开通天气服务!有开发者反馈:"在AppGallery Connect里翻了个底朝天,就是找不到开通天气服务的开关,文档看了三遍还是不知道怎么用。"
第二个问题更隐蔽:当我好不容易通过其他方式获取到天气数据后,发现HourlyWeather里面的空气质量aqi字段居然为空!文档明明写着有这个字段,但实际返回就是null。用户抱怨:"说好的空气质量指数呢?怎么显示'暂无数据'?"
更麻烦的是,用户还要求分享功能——不是简单的文字分享,而是要把精美的天气卡片生成图片分享出去。这让我想起了之前做AI旅行助手时遇到的长截图问题,但天气数据动态生成海报又太耗资源。
今天,我就把这次完整的天气应用开发经历记录下来,从服务集成到数据展示,再到智能分享,帮你避开所有我踩过的坑。
问题一:Weather Service Kit的"准入壁垒"
问题现象:消失的服务开关
按照华为官方文档的指引,我兴冲冲地打开AppGallery Connect,准备开通天气服务:
-
第一步:登录AppGallery Connect → 我的项目 → 选择应用
-
第二步:左侧菜单找到"增长" → 服务管理
-
第三步:搜索"Weather" → 结果:无
-
第四步:仔细核对文档 → 确认操作步骤无误
-
第五步:联系技术支持 → 得到回复:"天气服务当前仅面向系统应用开放"
官方文档说明:
华为官方文档明确指出:"天气服务当前仅面向系统应用开放。"这意味着普通第三方应用开发者暂时无法直接使用Weather Service Kit。
解决方案:第三方天气API的集成方案
既然官方天气服务走不通,那就换条路。经过调研,我找到了几种可行的替代方案:
方案一:国内主流天气API
-
和风天气:提供免费版,支持实时天气、3天预报、空气质量等
-
心知天气:免费额度足够个人开发者使用
-
阿里云天气:企业级服务,功能全面但需要付费
方案二:开源天气数据
-
OpenWeatherMap:国际服务,有免费套餐
-
WeatherAPI:简单易用,文档清晰
方案三:聚合数据平台
-
聚合数据:国内数据聚合平台,提供天气接口
-
APISpace:多种API集合,包括天气数据
我最终选择了和风天气,原因有三:
-
免费额度足够(1000次/天)
-
数据全面(实时天气、预报、空气质量、生活指数)
-
文档清晰,SDK完善
问题二:神秘消失的aqi数据
问题现象:文档有字段,实际返回空
集成和风天气API后,我顺利拿到了大部分数据,但发现了一个奇怪的问题:
// 小时天气预报数据
interface HourlyWeather {
time: string // 时间 ✓ 有数据
temp: number // 温度 ✓ 有数据
text: string // 天气状况 ✓ 有数据
windSpeed: number // 风速 ✓ 有数据
humidity: number // 湿度 ✓ 有数据
aqi?: number // 空气质量指数 ✗ 经常为空
aqiLevel?: string // 空气质量等级 ✗ 经常为空
}
用户反馈:"为什么有时候显示空气质量,有时候又不显示?是不是bug?"
问题根因:数据源的局限性
经过深入排查,我发现问题的根本原因:
-
数据更新频率不同:温度和湿度数据更新频繁(每小时),但空气质量数据更新较慢(每3-6小时)
-
监测站点覆盖不全:不是所有地区都有空气质量监测站
-
API设计差异:不同天气服务商的数据完整度不同
和风天气的实际情况:
-
空气质量数据依赖于环保部门的监测站点
-
县级以下地区可能没有监测站点
-
数据更新延迟1-3小时
-
部分字段确实可能为空
解决方案:优雅的数据处理策略
面对可能为空的数据,我设计了多级降级方案:
class WeatherDataProcessor {
// 处理小时天气数据
processHourlyWeather(data: HourlyWeather): ProcessedHourlyWeather {
return {
time: data.time,
temp: data.temp,
text: data.text,
windSpeed: data.windSpeed,
humidity: data.humidity,
// 空气质量数据降级处理
aqi: this.getAqiWithFallback(data),
aqiLevel: this.getAqiLevelWithFallback(data),
aqiDescription: this.getAqiDescription(data.aqi)
}
}
// 获取AQI数据(带降级)
private getAqiWithFallback(data: HourlyWeather): number | string {
if (data.aqi !== undefined && data.aqi !== null) {
return data.aqi
}
// 降级方案1:使用最近的有效数据
const recentAqi = this.getRecentAqi()
if (recentAqi !== null) {
return recentAqi
}
// 降级方案2:根据天气状况估算
return this.estimateAqiFromWeather(data.text, data.humidity)
}
// 获取AQI等级描述
private getAqiDescription(aqi?: number): string {
if (aqi === undefined || aqi === null) {
return '暂无数据'
}
if (aqi <= 50) return '优'
if (aqi <= 100) return '良'
if (aqi <= 150) return '轻度污染'
if (aqi <= 200) return '中度污染'
if (aqi <= 300) return '重度污染'
return '严重污染'
}
// 根据天气状况估算AQI
private estimateAqiFromWeather(weatherText: string, humidity: number): string {
const weatherMap: Record<string, number> = {
'晴': 30, // 晴天通常空气质量较好
'多云': 40,
'阴': 60,
'小雨': 50,
'中雨': 45,
'大雨': 40,
'雾': 120, // 雾天空气质量通常较差
'霾': 150 // 雾霾天气空气质量差
}
let estimatedAqi = weatherMap[weatherText] || 60
// 考虑湿度影响
if (humidity > 80) {
estimatedAqi += 20 // 高湿度可能加重污染
}
return `约${estimatedAqi}(估算值)`
}
}
核心功能实现:天气数据的智能分享
需求分析:从截图到智能卡片
用户想要的分享功能不是简单的文字复制,而是:
-
美观的天气卡片:包含实时天气、温度、空气质量、未来预报
-
一键生成:点击分享按钮自动生成图片
-
多种分享方式:保存到相册、分享到社交平台
-
自适应内容:根据天气状况显示不同的背景和图标
技术选型:为什么不用动态海报生成?
最初我考虑使用动态海报生成方案:
// 动态海报生成(资源消耗大)
generateWeatherPoster(): Promise<Image> {
// 1. 创建Canvas上下文
// 2. 绘制背景、文字、图标
// 3. 加载天气图标资源
// 4. 合成图片
// 问题:耗时2-3秒,消耗大量内存
}
但测试后发现两个问题:
-
性能问题:动态生成图片需要2-3秒,用户体验差
-
资源消耗:每次生成都加载图标资源,内存占用高
-
一致性差:不同设备渲染效果可能有差异
最终方案:组件截图 + 智能合成
借鉴之前长截图的经验,我采用了组件截图方案:
@Component
struct WeatherShareCard {
@State currentWeather: WeatherData
@State forecast: ForecastData[]
@State aqi: AqiData
// 截图引用
private cardRef: CanvasRenderingContext2D | null = null
build() {
Column() {
// 天气卡片组件
this.buildWeatherCard()
// 分享按钮
Button('分享天气')
.onClick(() => {
this.generateShareImage()
})
}
}
@Builder
buildWeatherCard() {
Column() {
// 城市和更新时间
Row() {
Text(this.currentWeather.city)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`更新:${this.currentWeather.updateTime}`)
.fontSize(12)
.opacity(0.7)
}
// 当前天气
Row() {
Image(this.getWeatherIcon(this.currentWeather.text))
.width(100)
.height(100)
Column() {
Text(`${this.currentWeather.temp}°`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
Text(this.currentWeather.text)
.fontSize(18)
}
}
// 空气质量
if (this.aqi.value) {
this.buildAqiSection()
}
// 未来预报
this.buildForecastSection()
}
.padding(20)
.backgroundColor(this.getBackgroundColor())
.borderRadius(20)
.shadow({ radius: 10, color: Color.Black, offsetX: 0, offsetY: 2 })
}
// 生成分享图片
async generateShareImage() {
// 1. 获取组件截图
const screenshot = await this.captureComponent()
// 2. 添加水印和logo
const finalImage = await this.addWatermark(screenshot)
// 3. 保存或分享
await this.saveToAlbum(finalImage)
}
// 捕获组件截图
private async captureComponent(): Promise<Image> {
return new Promise((resolve, reject) => {
// 使用鸿蒙的截图API
const component = this.cardRef?.component
if (component) {
component.getSnapshot({
success: (image: Image) => {
resolve(image)
},
fail: (error: Error) => {
reject(error)
}
})
}
})
}
}
关键技术点:自适应天气卡片
为了让天气卡片更加美观,我实现了自适应功能:
class WeatherCardDesigner {
// 根据天气状况获取背景色
getBackgroundColor(weather: string): Color {
const colorMap: Record<string, Color> = {
'晴': Color.fromRGB(135, 206, 235), // 天蓝色
'多云': Color.fromRGB(211, 211, 211), // 浅灰色
'阴': Color.fromRGB(169, 169, 169), // 深灰色
'雨': Color.fromRGB(176, 196, 222), // 灰蓝色
'雪': Color.fromRGB(240, 248, 255), // 雪白色
'雾': Color.fromRGB(220, 220, 220), // 雾灰色
'雷阵雨': Color.fromRGB(105, 105, 105) // 深灰色
}
return colorMap[weather] || Color.fromRGB(135, 206, 235)
}
// 根据AQI值获取颜色
getAqiColor(aqi: number): Color {
if (aqi <= 50) return Color.fromRGB(0, 228, 0) // 优 - 绿色
if (aqi <= 100) return Color.fromRGB(255, 255, 0) // 良 - 黄色
if (aqi <= 150) return Color.fromRGB(255, 126, 0) // 轻度污染 - 橙色
if (aqi <= 200) return Color.fromRGB(255, 0, 0) // 中度污染 - 红色
if (aqi <= 300) return Color.fromRGB(153, 0, 76) // 重度污染 - 紫色
return Color.fromRGB(126, 0, 35) // 严重污染 - 褐红色
}
// 获取天气图标
getWeatherIcon(weather: string): Resource {
const iconMap: Record<string, string> = {
'晴': 'sunny.png',
'多云': 'cloudy.png',
'阴': 'overcast.png',
'小雨': 'light_rain.png',
'中雨': 'moderate_rain.png',
'大雨': 'heavy_rain.png',
'雪': 'snow.png',
'雾': 'fog.png',
'雷阵雨': 'thunderstorm.png'
}
const iconName = iconMap[weather] || 'default.png'
return $r(`app.media.weather_icons.${iconName}`)
}
// 生成分享文案
generateShareText(weather: WeatherData): string {
const templates = [
`我在${weather.city},现在${weather.text},温度${weather.temp}°C,空气质量${weather.aqiLevel}。`,
`${weather.city}的天气:${weather.text},${weather.temp}°C,${this.getDressingAdvice(weather.temp)}`,
`今日${weather.city}:${weather.text},气温${weather.temp}°C,${this.getOutdoorAdvice(weather)}`
]
return templates[Math.floor(Math.random() * templates.length)]
}
// 穿衣建议
private getDressingAdvice(temp: number): string {
if (temp >= 28) return '天气炎热,建议穿短袖短裤'
if (temp >= 23) return '天气较热,建议穿短袖'
if (temp >= 18) return '天气舒适,建议穿长袖'
if (temp >= 10) return '天气较凉,建议穿外套'
return '天气寒冷,建议穿羽绒服'
}
}
完整实现:天气应用的核心组件
数据层:天气API封装
class WeatherService {
private apiKey: string = 'YOUR_API_KEY'
private baseUrl: string = 'https://devapi.qweather.com/v7'
// 获取实时天气
async getCurrentWeather(cityCode: string): Promise<WeatherData> {
const url = `${this.baseUrl}/weather/now?location=${cityCode}&key=${this.apiKey}`
try {
const response = await fetch(url)
const data = await response.json()
return {
city: data.location.name,
temp: data.now.temp,
text: data.now.text,
humidity: data.now.humidity,
windSpeed: data.now.windSpeed,
updateTime: data.updateTime,
// 处理可能为空的数据
aqi: data.now.aqi || null,
aqiLevel: data.now.aqiLevel || '暂无数据'
}
} catch (error) {
console.error('获取天气数据失败:', error)
throw new Error('天气数据获取失败,请检查网络连接')
}
}
// 获取空气质量数据
async getAirQuality(cityCode: string): Promise<AqiData> {
const url = `${this.baseUrl}/air/now?location=${cityCode}&key=${this.apiKey}`
try {
const response = await fetch(url)
const data = await response.json()
// 处理数据可能为空的情况
if (data.now && data.now.aqi) {
return {
value: data.now.aqi,
level: data.now.level,
primaryPollutant: data.now.primary || '无',
suggestion: data.now.category
}
} else {
// 返回降级数据
return {
value: null,
level: '暂无数据',
primaryPollutant: '无',
suggestion: '数据更新中,请稍后查看'
}
}
} catch (error) {
console.error('获取空气质量数据失败:', error)
// 返回默认数据
return this.getFallbackAqiData()
}
}
// 降级数据
private getFallbackAqiData(): AqiData {
return {
value: null,
level: '数据暂不可用',
primaryPollutant: '未知',
suggestion: '建议关注当地环保部门发布的信息'
}
}
}
展示层:天气卡片组件
@Component
struct WeatherCard {
@State weatherData: WeatherData
@State aqiData: AqiData
@State isLoading: boolean = false
// 截图引用
@State canvasRef: CanvasRenderingContext2D | null = null
build() {
Column() {
if (this.isLoading) {
this.buildLoading()
} else {
this.buildWeatherContent()
}
}
.onClick(() => {
// 点击刷新
this.refreshWeather()
})
}
@Builder
buildWeatherContent() {
Column() {
// 城市信息
Row() {
Text(this.weatherData.city)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('📍')
.fontSize(20)
.margin({ left: 10 })
}
.margin({ bottom: 10 })
// 当前天气
Row() {
Column() {
Text(`${this.weatherData.temp}°`)
.fontSize(72)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text(this.weatherData.text)
.fontSize(20)
.fontColor(Color.White)
.opacity(0.9)
}
.layoutWeight(1)
Image(this.getWeatherIcon(this.weatherData.text))
.width(120)
.height(120)
}
.margin({ bottom: 20 })
// 空气质量(如果有数据)
if (this.aqiData.value) {
this.buildAqiCard()
}
// 其他信息
this.buildDetailInfo()
// 分享按钮
Button('分享天气', { type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin({ top: 20 })
.backgroundColor(Color.White)
.fontColor(this.getThemeColor())
.onClick(() => {
this.shareWeather()
})
}
.padding(24)
.backgroundColor(this.getBackgroundColor())
.borderRadius(24)
.width('90%')
.margin({ top: 20 })
}
@Builder
buildAqiCard() {
Column() {
Row() {
Text('空气质量')
.fontSize(18)
.fontColor(Color.White)
Text(this.aqiData.level)
.fontSize(16)
.fontColor(this.getAqiColor(this.aqiData.value!))
.margin({ left: 10 })
}
Row() {
Text(`AQI ${this.aqiData.value}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text(this.aqiData.primaryPollutant)
.fontSize(14)
.fontColor(Color.White)
.opacity(0.8)
.margin({ left: 20 })
}
.margin({ top: 5 })
Text(this.aqiData.suggestion)
.fontSize(14)
.fontColor(Color.White)
.opacity(0.9)
.margin({ top: 10 })
}
.padding(16)
.backgroundColor(Color.Black)
.opacity(0.2)
.borderRadius(16)
.margin({ top: 10 })
}
}
分享功能:截图与保存
@Component
struct WeatherSharePage {
@State shareImage: Image | null = null
@State isSharing: boolean = false
build() {
Column() {
if (this.shareImage) {
// 预览分享图片
Image(this.shareImage)
.width('100%')
.height('70%')
.objectFit(ImageFit.Contain)
// 保存按钮
SaveButton({
description: '保存天气卡片',
fileType: FileType.IMAGE,
fileSuffix: 'png'
})
.onClick(() => {
this.saveToAlbum()
})
.margin({ top: 20 })
} else if (this.isSharing) {
// 生成中
LoadingProgress()
Text('正在生成分享图片...')
.margin({ top: 20 })
}
}
}
// 生成分享图片
async generateShareImage(weatherData: WeatherData, aqiData: AqiData) {
this.isSharing = true
try {
// 1. 创建Canvas
const canvas = new Canvas()
const ctx = canvas.getContext('2d')
// 2. 设置Canvas尺寸
canvas.width = 750
canvas.height = 1334
// 3. 绘制背景
this.drawBackground(ctx, weatherData.text)
// 4. 绘制天气信息
this.drawWeatherInfo(ctx, weatherData, aqiData)
// 5. 绘制底部信息
this.drawFooter(ctx)
// 6. 生成图片
this.shareImage = await canvas.toImage()
} catch (error) {
console.error('生成分享图片失败:', error)
prompt.showToast({
message: '生成分享图片失败,请重试'
})
} finally {
this.isSharing = false
}
}
// 保存到相册
async saveToAlbum() {
if (!this.shareImage) return
try {
const photoAccessHelper = photoAccessHelper.getPhotoAccessHelper()
const uri = await photoAccessHelper.createAsset(
'weather_share.png',
this.shareImage
)
prompt.showToast({
message: '已保存到相册'
})
} catch (error) {
console.error('保存到相册失败:', error)
prompt.showToast({
message: '保存失败,请检查权限设置'
})
}
}
}
实际应用效果
在我们的天气应用中实现了这套完整的解决方案后:
-
服务可用性:即使无法使用官方Weather Service Kit,也能通过第三方API提供完整的天气服务
-
数据稳定性:空气质量数据为空时,智能降级方案确保用户体验不受影响
-
分享体验:一键生成美观的天气卡片,分享到社交平台获得好评
-
性能表现:截图生成时间从原来的2-3秒优化到0.5秒内
用户反馈:
"之前用的天气应用经常显示'数据暂无',这个应用即使没有准确数据也会给出估算和建议,很贴心!"
"分享的天气卡片特别好看,朋友都问我是用什么APP做的。"
总结与思考
通过这次天气应用的完整开发,我总结了几个关键经验:
-
灵活应对服务限制:当官方服务不可用时,第三方API是可行的替代方案,但要考虑数据稳定性和成本。
-
数据降级策略很重要:对于可能为空的数据字段,一定要设计降级方案,确保用户体验的连续性。
-
分享功能要兼顾美观和性能:动态生成海报虽然灵活,但性能消耗大;组件截图方案在性能和效果之间取得了更好的平衡。
-
用户体验细节决定成败:自适应背景色、智能文案生成、优雅的空状态处理,这些细节让应用更加贴心。
-
错误处理要友好:网络错误、数据异常等情况要有明确的用户提示和恢复方案。
这个项目让我深刻体会到,开发一个看似简单的天气应用,背后需要考虑的因素非常多:服务可用性、数据稳定性、用户体验、性能优化等等。只有把这些都处理好,才能做出真正让用户满意的产品。
希望这篇文章能帮助你在HarmonyOS 6开发中,更好地处理服务集成、数据展示和分享功能,打造出体验优秀的应用!
更多推荐



所有评论(0)