天气页面很适合拿来做迁移练习:信息层级清楚,既有大字号数据,也有横向小时预报和纵向日期列表;等静态页面跑起来以后,还能继续接网络请求、加载态和失败重试。它看着不复杂,却不会简单到只剩一个 Text

我最开始的目标也很克制。先不急着接接口,把北京、28℃、小时预报和未来五天这些内容在 HarmonyOS 上完整排出来,确认 ArkUI 的布局方式和我过去熟悉的页面思路能对得上;静态壳稳定以后,再把另一页接到真实天气接口。这样出问题时能快速判断,到底是布局没有写对,还是网络链路没有走通。

在这里插入图片描述

先做一张能跑起来的天气首页

静态页并不是随手把文字写死在 build() 里。我先把小时天气和日期天气拆成两个数据模型。这个动作很小,却决定了后面列表能不能干净地渲染。

interface HourlyWeather {
  time: string
  temperature: number
  symbol: string
}

interface DailyWeather {
  day: string
  description: string
  low: number
  high: number
}

如果直接维护几个相互平行的数组,例如时间一个数组、温度一个数组、图标再来一个数组,第一版当然也能显示。但只要中间漏加一个元素,三个数组的索引就会错位。现在每个小时都是一个完整对象,timetemperaturesymbol 天然属于同一条记录;未来天气也同理。

完成接口定义后,我在 DevEco Studio 中继续补齐对应的小时数据和每日数据:

在这里插入图片描述

第一批数据暂时放在组件内部。小时数据用 HourlyWeather[],日期数据用 DailyWeather[],页面只关心怎样遍历

private hourlyItems: HourlyWeather[] = [
  { time: '现在', temperature: 28, symbol: '☁️' },
  { time: '15:00', temperature: 29, symbol: '🌤️' },
  { time: '16:00', temperature: 28, symbol: '🌤️' },
  { time: '17:00', temperature: 27, symbol: '☁️' },
  { time: '18:00', temperature: 26, symbol: '🌙' },
  { time: '19:00', temperature: 25, symbol: '🌙' }
]

private dailyItems: DailyWeather[] = [
  { day: '今天', description: '多云', low: 21, high: 29 },
  { day: '明天', description: '晴', low: 22, high: 31 },
  { day: '周六', description: '小雨', low: 20, high: 26 },
  { day: '周日', description: '多云', low: 21, high: 28 },
  { day: '周一', description: '晴', low: 23, high: 32 }
]

在这里插入图片描述

页面上真正重复的是“一个小时”和“一天”的视觉单元,所以我各写了一个 @BuilderHourlyItem() 使用竖向 Column,依次显示时间、天气符号和温度;DailyItem() 则使用 Row,让日期、天气说明、最低温和最高温排在一行。主页面只需要把数据交给 ForEach

@Builder
HourlyItem(item: HourlyWeather) {
  Column({ space: 6 }) {
    Text(item.time).fontSize(16).fontColor('#DCEBFF')
    Text(item.symbol).fontSize(30)
    Text(`${item.temperature}°`)
      .fontSize(20)
      .fontWeight(FontWeight.Medium)
      .fontColor(Color.White)
  }
  .width(70)
}

这里有一个很实际的布局选择:小时预报不能硬塞进屏幕宽度。我给每项一个固定宽度,再把外层 Row 放进横向 Scroll,小屏幕只显示当前能容纳的数量,剩余内容通过滑动查看。未来五天的数据方向相反,更适合放在 List 中纵向滚动。

Scroll() {
  Row() {
    ForEach(this.hourlyItems, (item: HourlyWeather) => {
      this.HourlyItem(item)
    }, (item: HourlyWeather) => item.time)
  }
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)

开始最容易犯的错误,是看到页面内容不多,就让最外层 Column 一路往下排。预览窗口里可能刚好放得下,到了较矮的设备上,日期列表底部就会被挤出视口。现在日期区域使用 layoutWeight(1) 占据剩余空间,自己的滚动由 List 负责,页面高度变化时不会把顶部的当前天气一起卷走。

这张是开发时同时保留工程树、ArkTS 代码和 Previewer 的画面。它比单独截一张漂亮页面更有用,因为可以直接看到 WeatherStatic.ets 里的 Builder 与右侧列表单元之间的对应关系。

在这里插入图片描述

静态版运行到设备后,顶部是两个阶段入口,中间用大字号显示当前温度,小时预报可以横向查看,下半部分保留五天卡片。到这一步,页面骨架才算真的站住,而不是停留在 Previewer 里“看起来差不多”。

在这里插入图片描述

静态数据不能假装成天气应用的终点

静态页解决的是布局迁移,但它没有解决数据从哪里来。第二阶段我新建 WeatherLive.ets,使用 @kit.NetworkKit 中的 http 发起请求。接口选择 Open-Meteo,是因为当前演示只需要公开的当前天气数据,不需要把密钥硬编码进工程。

实时页只维护界面真正需要的状态:温度、天气描述、风速、是否正在加载,以及错误文本。

@State temperature: string = '--'
@State description: string = '等待请求'
@State windSpeed: string = '--'
@State loading: boolean = false
@State errorMessage: string = ''

温度和风速使用字符串,是因为请求开始前需要显示 --loading 决定页面是否展示 LoadingProgresserrorMessage 既保存失败原因,也充当失败视图的开关。通过这几个状态,成功、加载和失败三个画面可以保持互斥。

网络请求前还要在 module.json5 里声明联网权限。缺少这段配置时,UI 可以正常构建,点击刷新却拿不到远端数据,这类问题只盯着 ArkTS 页面很难发现。

"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

请求函数里我先挡住重复触发。用户连续点击刷新时,如果上一次还没结束,就直接返回;随后清空旧错误、创建请求对象,再发起 GET 请求。

private async fetchWeather(): Promise<void> {
  if (this.loading) {
    return // 上一次请求未结束,避免叠加刷新
  }

  this.loading = true
  this.errorMessage = ''
  const request = http.createHttp()

  try {
    const response = await request.request(url, {
      method: http.RequestMethod.GET,
      expectDataType: http.HttpDataType.STRING,
      connectTimeout: 10000,
      readTimeout: 10000
    })

    if (response.responseCode !== 200) {
      this.errorMessage = `服务器返回 ${response.responseCode}`
      return
    }

    const result = JSON.parse(response.result.toString()) as WeatherResponse
    this.temperature = Math.round(result.current.temperature_2m).toString()
    this.description = weatherCodeToText(result.current.weather_code)
    this.windSpeed = result.current.wind_speed_10m.toFixed(1)
  } catch (error) {
    this.errorMessage = `请求失败:${JSON.stringify(error)}`
  } finally {
    request.destroy() // 无论成功失败都释放请求资源
    this.loading = false
  }
}

网络功能在本地最容易出现一种“假完成”:接口通的时候一切正常,一断网,页面永远停在转圈,刷新按钮也回不来。现在非200响应会留下“服务器返回 xxx”,异常会进入 catch,而 finally 一定会销毁请求并恢复 loading。即使请求失败,页面仍然能进入明确的错误视图,用户可以点击“重试”。

JSON 解析也没有直接到处访问动态对象,而是给响应补了最小接口:

interface CurrentWeather {
  temperature_2m: number
  weather_code: number
  wind_speed_10m: number
}

interface WeatherResponse {
  current: CurrentWeather
}

类型只描述本页用到的字段,不照着接口完整响应抄一遍。这样 result.current.temperature_2m 的含义明确,字段拼错时也更容易在开发阶段发现。

接口返回的是天气代码,不是“晴”“多云”这样的中文文案。我写了一个小转换函数,根据代码区间映射说明。当前实现够本页展示,但并没有假装覆盖所有气象细节;如果以后增加图标和降水概率,这个函数应该进一步变成完整映射表,而不是继续堆条件。

两个阶段放进同一个可运行入口

原本静态页和实时页可以写成两篇,但从开发过程看,它们其实是同一次迁移的前后阶段。拆开以后,第一篇只有UI,第二篇又要重复解释工程和页面。因此我在 Index.ets 中保留一个很薄的阶段切换层,用 currentDemo 控制显示哪个组件。

@State currentDemo: number = 0

if (this.currentDemo === 0) {
  WeatherStatic().layoutWeight(1)
} else {
  WeatherLive().layoutWeight(1)
}

顶部两个按钮只是开发入口,不是为了模拟完整天气产品的导航。它的价值是让同一个 HAP 可以现场对照:静态版验证信息架构,实时版验证网络数据、加载状态和刷新动作。二者的背景与排版保持相近,切换时能明显看出数据来源变了,而不是换成另一个毫无关系的页面。

实际运行实时页时,请求拿到了北京当前天气,页面显示31℃、多云和风速3.2 km/h。这里的数字来自当次请求,和静态页里用于布局的28℃不是同一份数据;“刷新数据”按钮也已经回到可操作状态,说明请求结束后的 finally 分支执行完成。

在这里插入图片描述

当前运行结果说明成功链路已经走通:请求返回数据,JSON完成解析,页面也从加载态切换到了内容态。网络异常仍需要单独验证,可以通过断开网络或临时替换为不可访问地址复现,再检查页面是否进入“请求失败”并允许重试。当前没有记录具体错误码,因此这里只保留已经实现的异常处理逻辑。

当前实现的边界

当前城市和经纬度固定为北京,还没有接入定位权限与天气缓存,逐小时真实接口数据也没有并入静态首页;静态页中的小时预报和五日预报仍然用于验证布局。增加城市选择时,需要把经纬度从请求函数中抽出,并给刷新、切换城市和缓存读取定义清楚的优先级。

这次开发把几个容易混在一起的问题拆开处理:先用类型化数据和 Builder 稳住重复界面,再让横向小时预报与纵向日期列表分别管理滚动;网络阶段单独处理权限、加载、非200响应、异常和资源释放;入口层只负责保留静态版和实时版两个阶段。继续增加城市搜索或缓存时,不需要推翻现有页面结构。

Logo

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

更多推荐