目录
-
- 前言
- 一、项目背景与功能定位
-
- [1.1 功能概览](#1.1 功能概览)
- [1.2 技术关键词](#1.2 技术关键词)
- [1.3 数据到视觉的链路](#1.3 数据到视觉的链路)
- 二、项目目录结构分析
-
- [2.1 根目录结构](#2.1 根目录结构)
- [2.2 核心文件说明](#2.2 核心文件说明)
- [2.3 OpenHarmony 工程结构](#2.3 OpenHarmony 工程结构)
- 三、环境与依赖配置
-
- [3.1 pubspec 基础信息](#3.1 pubspec 基础信息)
- [3.2 依赖配置](#3.2 依赖配置)
- [3.3 常用命令](#3.3 常用命令)
- 四、应用入口源码拆解
-
- [4.1 main 函数](#4.1 main 函数)
- [4.2 WeatherApp](#4.2 WeatherApp)
- [4.3 Material 3 主题](#4.3 Material 3 主题)
- [五、WeatherData 数据模型](#五、WeatherData 数据模型)
-
- [5.1 模型源码](#5.1 模型源码)
- [5.2 字段说明](#5.2 字段说明)
- [5.3 模型与 UI 的关系](#5.3 模型与 UI 的关系)
- 六、页面状态设计
-
- [6.1 WeatherHomePage](#6.1 WeatherHomePage)
- [6.2 状态字段](#6.2 状态字段)
- [6.3 状态组合](#6.3 状态组合)
- 七、样例天气数据
-
- [7.1 内置数据列表](#7.1 内置数据列表)
- [7.2 样例数据表](#7.2 样例数据表)
- [7.3 模拟数据的意义](#7.3 模拟数据的意义)
- 八、搜索逻辑拆解
-
- [8.1 _searchWeather 方法](#8.1 _searchWeather 方法)
- [8.2 输入处理](#8.2 输入处理)
- [8.3 加载状态](#8.3 加载状态)
- [8.4 样例匹配](#8.4 样例匹配)
- [8.5 查询完成](#8.5 查询完成)
- 九、兜底天气生成
-
- [9.1 orElse 逻辑](#9.1 orElse 逻辑)
- [9.2 生成规则](#9.2 生成规则)
- [9.3 示例](#9.3 示例)
- 十、天气图标映射
-
- [10.1 _getWeatherIcon 方法](#10.1 _getWeatherIcon 方法)
- [10.2 图标映射表](#10.2 图标映射表)
- [10.3 UI 使用位置](#10.3 UI 使用位置)
- 十一、天气颜色映射
-
- [11.1 _getWeatherColor 方法](#11.1 _getWeatherColor 方法)
- [11.2 颜色映射表](#11.2 颜色映射表)
- [11.3 渐变背景](#11.3 渐变背景)
- 十二、页面输入区实现
-
- [12.1 输入区结构](#12.1 输入区结构)
- [12.2 TextField 配置](#12.2 TextField 配置)
- [12.3 搜索按钮](#12.3 搜索按钮)
- 十三、加载与错误展示
-
- [13.1 加载指示器](#13.1 加载指示器)
- [13.2 错误展示入口](#13.2 错误展示入口)
- [13.3 天气卡片展示条件](#13.3 天气卡片展示条件)
- [十四、天气卡片 UI 实现](#十四、天气卡片 UI 实现)
-
- [14.1 Card 容器](#14.1 Card 容器)
- [14.2 渐变 Container](#14.2 渐变 Container)
- [14.3 信息层级](#14.3 信息层级)
- [14.4 字体层级](#14.4 字体层级)
- 十五、湿度与风速信息块
-
- [15.1 _buildWeatherInfo 方法](#15.1 _buildWeatherInfo 方法)
- [15.2 调用方式](#15.2 调用方式)
- [15.3 信息块结构](#15.3 信息块结构)
- [十六、OpenHarmony 适配边界](#十六、OpenHarmony 适配边界)
-
- [16.1 Flutter 层职责](#16.1 Flutter 层职责)
- [16.2 平台侧文件作用](#16.2 平台侧文件作用)
- [16.3 天气应用的适配特点](#16.3 天气应用的适配特点)
- 十七、测试与验证
-
- [17.1 交互验证路径](#17.1 交互验证路径)
- [17.2 Widget 测试示例](#17.2 Widget 测试示例)
- [17.3 搜索样例城市测试](#17.3 搜索样例城市测试)
- [17.4 搜索兜底城市测试](#17.4 搜索兜底城市测试)
- 十八、常见问题解析
-
- [18.1 为什么使用 Future.delayed](#18.1 为什么使用 Future.delayed)
- [18.2 为什么使用 firstWhere](#18.2 为什么使用 firstWhere)
- [18.3 为什么城市匹配要转小写](#18.3 为什么城市匹配要转小写)
- [18.4 为什么卡片颜色跟随天气变化](#18.4 为什么卡片颜色跟随天气变化)
- [18.5 为什么当前不需要权限](#18.5 为什么当前不需要权限)
- 十九、完整业务链路复盘
-
- [19.1 搜索链路](#19.1 搜索链路)
- [19.2 映射链路](#19.2 映射链路)
- [19.3 渲染链路](#19.3 渲染链路)
- 二十、核心源码总览
- 二十一、总结
- 相关资源
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
weather 是一个基于 Flutter 实现的 Weather 天气查询应用 。项目核心代码集中在 lib/main.dart,同时保留了 ohos OpenHarmony 平台工程目录,适合作为 Flutter 输入搜索、模拟数据、加载状态和渐变卡片 UI 的入门实战案例。
这个项目没有接入真实天气接口,而是通过内置样例数据和兜底数据生成逻辑模拟天气查询流程。正因为它不依赖网络服务,读者可以把注意力集中在 Flutter 页面状态、数据模型、UI 映射和 OpenHarmony 工程承载上。
本文围绕当前项目真实源码展开,重点拆解:
WeatherApp如何配置应用入口和浅蓝色主题。WeatherData如何表达城市、温度、天气、湿度和风速。_searchWeather()如何处理输入、加载状态、样例匹配和兜底数据。_getWeatherIcon()与_getWeatherColor()如何把天气状况映射到视觉元素。- 渐变天气卡片如何展示图标、温度、湿度和风速。
- Flutter 页面如何在 OpenHarmony 工程中被承载。
效果图如下:

一、项目背景与功能定位
1.1 功能概览
weather 是一个轻量天气查询应用。用户在输入框中输入城市名称,点击搜索按钮或按键盘提交后,页面进入加载状态。加载结束后,应用会展示城市天气卡片,包括天气图标、城市名、温度、天气状况、湿度和风速。
当前项目支持以下功能:
- 输入城市名称。
- 点击搜索按钮触发查询。
- 键盘提交触发查询。
- 搜索期间显示
CircularProgressIndicator。 - 内置纽约、伦敦、东京、巴黎、悉尼、北京等示例天气数据。
- 未命中示例城市时,根据城市名长度生成兜底天气。
- 根据天气状况显示不同图标。
- 根据天气状况生成不同主题色。
- 使用
LinearGradient构建天气卡片背景。 - 展示湿度和风速两个信息块。
1.2 技术关键词
| 关键词 | 在项目中的作用 |
|---|---|
WeatherData |
天气数据模型 |
TextEditingController |
读取城市输入 |
Future.delayed |
模拟异步查询等待 |
firstWhere |
从样例数据中查找城市 |
orElse |
未命中样例数据时生成兜底数据 |
IconData |
映射天气图标 |
Color |
映射天气主题色 |
LinearGradient |
构建渐变天气卡片 |
CircularProgressIndicator |
展示加载状态 |
withValues(alpha: 0.7) |
生成透明度变化的渐变色 |
1.3 数据到视觉的链路
#mermaid-svg-Onwqkp8OeyhrLfV7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Onwqkp8OeyhrLfV7 .error-icon{fill:#552222;}#mermaid-svg-Onwqkp8OeyhrLfV7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Onwqkp8OeyhrLfV7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .marker.cross{stroke:#333333;}#mermaid-svg-Onwqkp8OeyhrLfV7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Onwqkp8OeyhrLfV7 p{margin:0;}#mermaid-svg-Onwqkp8OeyhrLfV7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster-label text{fill:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster-label span{color:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster-label span p{background-color:transparent;}#mermaid-svg-Onwqkp8OeyhrLfV7 .label text,#mermaid-svg-Onwqkp8OeyhrLfV7 span{fill:#333;color:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .node rect,#mermaid-svg-Onwqkp8OeyhrLfV7 .node circle,#mermaid-svg-Onwqkp8OeyhrLfV7 .node ellipse,#mermaid-svg-Onwqkp8OeyhrLfV7 .node polygon,#mermaid-svg-Onwqkp8OeyhrLfV7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .rough-node .label text,#mermaid-svg-Onwqkp8OeyhrLfV7 .node .label text,#mermaid-svg-Onwqkp8OeyhrLfV7 .image-shape .label,#mermaid-svg-Onwqkp8OeyhrLfV7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Onwqkp8OeyhrLfV7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .rough-node .label,#mermaid-svg-Onwqkp8OeyhrLfV7 .node .label,#mermaid-svg-Onwqkp8OeyhrLfV7 .image-shape .label,#mermaid-svg-Onwqkp8OeyhrLfV7 .icon-shape .label{text-align:center;}#mermaid-svg-Onwqkp8OeyhrLfV7 .node.clickable{cursor:pointer;}#mermaid-svg-Onwqkp8OeyhrLfV7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .arrowheadPath{fill:#333333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Onwqkp8OeyhrLfV7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Onwqkp8OeyhrLfV7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Onwqkp8OeyhrLfV7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster text{fill:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 .cluster span{color:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Onwqkp8OeyhrLfV7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Onwqkp8OeyhrLfV7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Onwqkp8OeyhrLfV7 .icon-shape,#mermaid-svg-Onwqkp8OeyhrLfV7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Onwqkp8OeyhrLfV7 .icon-shape p,#mermaid-svg-Onwqkp8OeyhrLfV7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Onwqkp8OeyhrLfV7 .icon-shape .label rect,#mermaid-svg-Onwqkp8OeyhrLfV7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Onwqkp8OeyhrLfV7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Onwqkp8OeyhrLfV7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Onwqkp8OeyhrLfV7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
用户输入城市
_searchWeather()
trim 城市名
城市名为空?
直接返回
_isLoading = true
Future.delayed 1 秒
sampleWeather.firstWhere
命中样例城市?
返回样例 WeatherData
orElse 生成兜底 WeatherData
_weather = found
_isLoading = false
天气卡片重新渲染
这条流程说明,天气页面的核心不只是搜索输入,而是从输入文本出发,生成结构化数据,再把数据映射成图标、颜色和布局。
关键点:天气应用很适合讲 Flutter 的"数据驱动 UI"。城市、温度、天气状况、湿度、风速最终都会转化为页面上的文字、图标、颜色和卡片结构。
二、项目目录结构分析
2.1 根目录结构
项目采用 Flutter 标准目录,同时包含 OpenHarmony 平台工程。
bash
weather/
├── lib/
│ └── main.dart
├── ohos/
│ ├── AppScope/
│ └── entry/
├── test/
│ └── widget_test.dart
├── pubspec.yaml
├── analysis_options.yaml
└── README.md
2.2 核心文件说明
| 文件或目录 | 作用 | 本文重点 |
|---|---|---|
lib/main.dart |
应用入口、天气模型、搜索逻辑和 UI | 核心源码 |
pubspec.yaml |
项目名称、版本、依赖和 Flutter 配置 | 环境依赖 |
analysis_options.yaml |
Dart 静态分析规则 | 代码规范 |
test/ |
Flutter 测试目录 | 输入与渲染验证 |
ohos/ |
OpenHarmony 平台工程 | 平台承载 |
2.3 OpenHarmony 工程结构
ohos 目录通常包含应用级配置、模块级配置、Ability 入口、页面入口和资源文件。当前天气项目的业务逻辑保持在 Flutter 层,OpenHarmony 层负责承载应用。
| OpenHarmony 文件 | 作用 |
|---|---|
AppScope/app.json5 |
应用级配置 |
entry/src/main/module.json5 |
模块级配置 |
EntryAbility.ets |
Ability 生命周期入口 |
Index.ets |
页面承载入口 |
GeneratedPluginRegistrant.ets |
插件注册入口 |
resources/base/element/string.json |
字符串资源 |
resources/base/media/icon.png |
图标资源 |
三、环境与依赖配置
3.1 pubspec 基础信息
当前项目名称为 weather,版本是 1.0.0+1,Dart SDK 约束是 ^3.9.2。
yaml
name: weather
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.9.2
publish_to: 'none' 表示这是应用工程,不会被误发布到 pub.dev。
3.2 依赖配置
项目依赖非常轻量。
yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
当前版本没有网络请求库,也没有天气 API SDK。天气结果由本地样例数据和兜底算法生成。
3.3 常用命令
bash
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 作用 |
|---|---|
flutter pub get |
获取项目依赖 |
flutter analyze |
执行静态分析 |
flutter test |
运行测试 |
flutter run |
启动调试运行 |
flutter build hap |
构建 OpenHarmony HAP 包 |
四、应用入口源码拆解
4.1 main 函数
Flutter 应用从 main() 函数启动。
dart
import 'package:flutter/material.dart';
void main() {
runApp(const WeatherApp());
}
runApp(const WeatherApp()) 将 WeatherApp 放入 Flutter 渲染树根节点。
4.2 WeatherApp
WeatherApp 是应用根组件,负责配置应用标题、主题和首页。
dart
class WeatherApp extends StatelessWidget {
const WeatherApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Weather',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue),
useMaterial3: true,
),
home: const WeatherHomePage(title: 'Weather'),
);
}
}
这里有三个关键信息:
- 应用标题是
Weather。 - 主题色使用
Colors.lightBlue,与天气类应用气质一致。 - 首页是
WeatherHomePage,天气搜索状态从首页 State 中维护。
4.3 Material 3 主题
dart
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue),
useMaterial3: true,
),
useMaterial3: true 会影响 AppBar、TextField、ElevatedButton、Card 等组件的默认视觉效果。当前项目在天气卡片中进一步使用渐变色,让不同天气状态具有更直观的识别度。
五、WeatherData 数据模型
5.1 模型源码
当前项目使用 WeatherData 表达一条天气数据。
dart
class WeatherData {
final String city;
final int temperature;
final String condition;
final int humidity;
final double windSpeed;
WeatherData({
required this.city,
required this.temperature,
required this.condition,
required this.humidity,
required this.windSpeed,
});
}
这个模型使用 final 字段,说明天气数据创建后不会在对象内部修改。页面切换城市时,会创建新的 WeatherData 并赋值给 _weather。
5.2 字段说明
| 字段 | 类型 | 含义 | UI 展示位置 |
|---|---|---|---|
city |
String |
城市名称 | 卡片城市标题 |
temperature |
int |
温度,单位摄氏度 | 大号温度文本 |
condition |
String |
天气状况 | 图标、颜色、天气文本 |
humidity |
int |
湿度百分比 | 湿度信息块 |
windSpeed |
double |
风速,单位 km/h | 风速信息块 |
5.3 模型与 UI 的关系
#mermaid-svg-wGy6hO45mlyTeE3i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wGy6hO45mlyTeE3i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wGy6hO45mlyTeE3i .error-icon{fill:#552222;}#mermaid-svg-wGy6hO45mlyTeE3i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wGy6hO45mlyTeE3i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wGy6hO45mlyTeE3i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wGy6hO45mlyTeE3i .marker.cross{stroke:#333333;}#mermaid-svg-wGy6hO45mlyTeE3i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wGy6hO45mlyTeE3i p{margin:0;}#mermaid-svg-wGy6hO45mlyTeE3i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wGy6hO45mlyTeE3i .cluster-label text{fill:#333;}#mermaid-svg-wGy6hO45mlyTeE3i .cluster-label span{color:#333;}#mermaid-svg-wGy6hO45mlyTeE3i .cluster-label span p{background-color:transparent;}#mermaid-svg-wGy6hO45mlyTeE3i .label text,#mermaid-svg-wGy6hO45mlyTeE3i span{fill:#333;color:#333;}#mermaid-svg-wGy6hO45mlyTeE3i .node rect,#mermaid-svg-wGy6hO45mlyTeE3i .node circle,#mermaid-svg-wGy6hO45mlyTeE3i .node ellipse,#mermaid-svg-wGy6hO45mlyTeE3i .node polygon,#mermaid-svg-wGy6hO45mlyTeE3i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wGy6hO45mlyTeE3i .rough-node .label text,#mermaid-svg-wGy6hO45mlyTeE3i .node .label text,#mermaid-svg-wGy6hO45mlyTeE3i .image-shape .label,#mermaid-svg-wGy6hO45mlyTeE3i .icon-shape .label{text-anchor:middle;}#mermaid-svg-wGy6hO45mlyTeE3i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wGy6hO45mlyTeE3i .rough-node .label,#mermaid-svg-wGy6hO45mlyTeE3i .node .label,#mermaid-svg-wGy6hO45mlyTeE3i .image-shape .label,#mermaid-svg-wGy6hO45mlyTeE3i .icon-shape .label{text-align:center;}#mermaid-svg-wGy6hO45mlyTeE3i .node.clickable{cursor:pointer;}#mermaid-svg-wGy6hO45mlyTeE3i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wGy6hO45mlyTeE3i .arrowheadPath{fill:#333333;}#mermaid-svg-wGy6hO45mlyTeE3i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wGy6hO45mlyTeE3i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wGy6hO45mlyTeE3i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wGy6hO45mlyTeE3i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wGy6hO45mlyTeE3i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wGy6hO45mlyTeE3i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wGy6hO45mlyTeE3i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wGy6hO45mlyTeE3i .cluster text{fill:#333;}#mermaid-svg-wGy6hO45mlyTeE3i .cluster span{color:#333;}#mermaid-svg-wGy6hO45mlyTeE3i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wGy6hO45mlyTeE3i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wGy6hO45mlyTeE3i rect.text{fill:none;stroke-width:0;}#mermaid-svg-wGy6hO45mlyTeE3i .icon-shape,#mermaid-svg-wGy6hO45mlyTeE3i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wGy6hO45mlyTeE3i .icon-shape p,#mermaid-svg-wGy6hO45mlyTeE3i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wGy6hO45mlyTeE3i .icon-shape .label rect,#mermaid-svg-wGy6hO45mlyTeE3i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wGy6hO45mlyTeE3i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wGy6hO45mlyTeE3i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wGy6hO45mlyTeE3i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WeatherData.city
城市标题
WeatherData.temperature
温度文本
WeatherData.condition
天气图标
渐变颜色
天气状况文本
WeatherData.humidity
湿度信息块
WeatherData.windSpeed
风速信息块
WeatherData 是页面展示的唯一业务数据来源。只要 _weather 更新,天气卡片就会根据新数据重建。
六、页面状态设计
6.1 WeatherHomePage
首页组件接收页面标题,并创建状态对象。
dart
class WeatherHomePage extends StatefulWidget {
const WeatherHomePage({super.key, required this.title});
final String title;
@override
State<WeatherHomePage> createState() => _WeatherHomePageState();
}
天气查询会改变加载状态和展示数据,因此首页使用 StatefulWidget。
6.2 状态字段
dart
class _WeatherHomePageState extends State<WeatherHomePage> {
final TextEditingController _cityController = TextEditingController();
WeatherData? _weather;
bool _isLoading = false;
String? _error;
}
| 字段 | 类型 | 作用 |
|---|---|---|
_cityController |
TextEditingController |
管理城市输入框 |
_weather |
WeatherData? |
保存当前查询结果 |
_isLoading |
bool |
控制加载指示器 |
_error |
String? |
预留错误信息展示 |
6.3 状态组合
| 状态组合 | 页面表现 |
|---|---|
_weather == null && !_isLoading |
只显示输入区 |
_isLoading == true |
显示加载指示器 |
_weather != null && !_isLoading |
显示天气卡片 |
_error != null |
显示红色错误文本 |
当前源码中 _error 会在搜索开始时清空,但没有实际赋值错误信息。这保留了错误展示入口,也不会影响当前模拟查询流程。
七、样例天气数据
7.1 内置数据列表
项目内置了 6 个城市的天气数据。
dart
final List<WeatherData> _sampleWeather = [
WeatherData(city: 'New York', temperature: 22, condition: 'Sunny', humidity: 45, windSpeed: 12),
WeatherData(city: 'London', temperature: 15, condition: 'Cloudy', humidity: 78, windSpeed: 20),
WeatherData(city: 'Tokyo', temperature: 28, condition: 'Rainy', humidity: 85, windSpeed: 15),
WeatherData(city: 'Paris', temperature: 18, condition: 'Partly Cloudy', humidity: 55, windSpeed: 10),
WeatherData(city: 'Sydney', temperature: 25, condition: 'Clear', humidity: 40, windSpeed: 8),
WeatherData(city: 'Beijing', temperature: 30, condition: 'Hazy', humidity: 60, windSpeed: 5),
];
7.2 样例数据表
| 城市 | 温度 | 天气 | 湿度 | 风速 |
|---|---|---|---|---|
| New York | 22°C | Sunny | 45% | 12 km/h |
| London | 15°C | Cloudy | 78% | 20 km/h |
| Tokyo | 28°C | Rainy | 85% | 15 km/h |
| Paris | 18°C | Partly Cloudy | 55% | 10 km/h |
| Sydney | 25°C | Clear | 40% | 8 km/h |
| Beijing | 30°C | Hazy | 60% | 5 km/h |
7.3 模拟数据的意义
内置数据让页面能够脱离网络 API 直接运行。对于入门项目来说,这种方式可以先把 UI、状态和交互链路跑通,再逐步替换成真实接口。
天气查询类应用可以先用本地数据完成页面闭环,再接入真实 API。这样开发过程更稳,问题也更容易定位。
八、搜索逻辑拆解
8.1 _searchWeather 方法
搜索逻辑集中在 _searchWeather() 中。
dart
void _searchWeather() {
final city = _cityController.text.trim();
if (city.isEmpty) return;
setState(() {
_isLoading = true;
_error = null;
});
Future.delayed(const Duration(seconds: 1), () {
final found = _sampleWeather.firstWhere(
(w) => w.city.toLowerCase() == city.toLowerCase(),
orElse: () => WeatherData(
city: city,
temperature: 20 + (city.length * 2) % 15,
condition: ['Sunny', 'Cloudy', 'Rainy', 'Clear', 'Windy'][city.length % 5],
humidity: 40 + (city.length * 7) % 50,
windSpeed: 5 + (city.length * 3) % 20,
),
);
setState(() {
_weather = found;
_isLoading = false;
});
});
}
这个方法包含输入校验、加载状态、模拟延迟、样例匹配、兜底生成和状态回写。
8.2 输入处理
dart
final city = _cityController.text.trim();
if (city.isEmpty) return;
trim() 会去掉首尾空格,避免用户输入空白字符时触发无效查询。城市名为空时直接返回,不显示加载状态。
8.3 加载状态
dart
setState(() {
_isLoading = true;
_error = null;
});
搜索开始时,页面进入加载状态,并清空旧错误。随后 Future.delayed 模拟 1 秒查询等待。
8.4 样例匹配
dart
final found = _sampleWeather.firstWhere(
(w) => w.city.toLowerCase() == city.toLowerCase(),
orElse: () => WeatherData(...),
);
这里使用大小写不敏感匹配。输入 beijing、Beijing 或 BEIJING 都能命中 Beijing 样例数据。
8.5 查询完成
dart
setState(() {
_weather = found;
_isLoading = false;
});
查询完成后,_weather 获得结果,加载状态关闭,天气卡片开始展示。
九、兜底天气生成
9.1 orElse 逻辑
当输入城市不在 _sampleWeather 中时,orElse 会生成一条新的 WeatherData。
dart
orElse: () => WeatherData(
city: city,
temperature: 20 + (city.length * 2) % 15,
condition: ['Sunny', 'Cloudy', 'Rainy', 'Clear', 'Windy'][city.length % 5],
humidity: 40 + (city.length * 7) % 50,
windSpeed: 5 + (city.length * 3) % 20,
),
这种写法保证任何非空城市名都能得到天气结果,页面不会因为找不到城市而中断。
9.2 生成规则
| 字段 | 生成规则 | 值范围或来源 |
|---|---|---|
city |
使用用户输入 | 输入城市名 |
temperature |
20 + (city.length * 2) % 15 |
20°C 到 34°C |
condition |
天气数组按长度取模 | Sunny、Cloudy、Rainy、Clear、Windy |
humidity |
40 + (city.length * 7) % 50 |
40% 到 89% |
windSpeed |
5 + (city.length * 3) % 20 |
5 到 24 km/h |
9.3 示例
dart
final city = 'Shanghai';
final weather = WeatherData(
city: city,
temperature: 20 + (city.length * 2) % 15,
condition: ['Sunny', 'Cloudy', 'Rainy', 'Clear', 'Windy'][city.length % 5],
humidity: 40 + (city.length * 7) % 50,
windSpeed: 5 + (city.length * 3) % 20,
);
这段逻辑让 Shanghai 这样的非内置城市也能显示完整天气卡片。
十、天气图标映射
10.1 _getWeatherIcon 方法
不同天气状况会映射到不同 Material 图标。
dart
IconData _getWeatherIcon(String condition) {
switch (condition.toLowerCase()) {
case 'sunny':
case 'clear':
return Icons.wb_sunny;
case 'cloudy':
case 'partly cloudy':
return Icons.cloud;
case 'rainy':
return Icons.grain;
case 'windy':
return Icons.air;
case 'hazy':
return Icons.blur_on;
default:
return Icons.cloud_queue;
}
}
10.2 图标映射表
| 天气条件 | 图标 |
|---|---|
Sunny |
Icons.wb_sunny |
Clear |
Icons.wb_sunny |
Cloudy |
Icons.cloud |
Partly Cloudy |
Icons.cloud |
Rainy |
Icons.grain |
Windy |
Icons.air |
Hazy |
Icons.blur_on |
| 其他 | Icons.cloud_queue |
10.3 UI 使用位置
dart
Icon(
_getWeatherIcon(_weather!.condition),
size: 80,
color: Colors.white,
),
图标尺寸为 80,颜色为白色,位于天气卡片顶部,是用户看到天气状态的第一视觉信号。
十一、天气颜色映射
11.1 _getWeatherColor 方法
天气状况不仅影响图标,也影响卡片渐变色。
dart
Color _getWeatherColor(String condition) {
switch (condition.toLowerCase()) {
case 'sunny':
case 'clear':
return Colors.orange;
case 'cloudy':
case 'partly cloudy':
return Colors.grey;
case 'rainy':
return Colors.blue;
case 'windy':
return Colors.teal;
case 'hazy':
return Colors.amber;
default:
return Colors.blueGrey;
}
}
11.2 颜色映射表
| 天气条件 | 主题色 |
|---|---|
Sunny / Clear |
Colors.orange |
Cloudy / Partly Cloudy |
Colors.grey |
Rainy |
Colors.blue |
Windy |
Colors.teal |
Hazy |
Colors.amber |
| 其他 | Colors.blueGrey |
11.3 渐变背景
dart
gradient: LinearGradient(
colors: [
_getWeatherColor(_weather!.condition),
_getWeatherColor(_weather!.condition).withValues(alpha: 0.7),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
卡片使用同一主题色的不同透明度形成纵向渐变。这样既能强化天气氛围,又不会引入复杂图片资源。
十二、页面输入区实现
12.1 输入区结构
页面顶部使用 Row 放置输入框和搜索按钮。
dart
Row(
children: [
Expanded(
child: TextField(
controller: _cityController,
decoration: InputDecoration(
hintText: 'Enter city name...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.location_city),
),
onSubmitted: (_) => _searchWeather(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _searchWeather,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Icon(Icons.search),
),
],
)
Expanded 让输入框占据剩余宽度,按钮固定在右侧。
12.2 TextField 配置
dart
TextField(
controller: _cityController,
decoration: InputDecoration(
hintText: 'Enter city name...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.location_city),
),
onSubmitted: (_) => _searchWeather(),
)
输入框使用城市图标作为前缀,提示语为 Enter city name...。用户按下键盘提交键时,也会触发 _searchWeather()。
12.3 搜索按钮
dart
ElevatedButton(
onPressed: _searchWeather,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Icon(Icons.search),
)
搜索按钮使用放大镜图标,和输入框保持相同圆角风格。
十三、加载与错误展示
13.1 加载指示器
搜索期间页面展示 CircularProgressIndicator。
dart
if (_isLoading) const CircularProgressIndicator(),
它和 _isLoading 绑定。搜索开始时显示,查询完成后隐藏。
13.2 错误展示入口
dart
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red, fontSize: 16)),
当前模拟查询逻辑不会设置 _error,但页面已经预留了错误展示区域。后续接入真实 API 时,可以把网络异常或接口错误写入 _error,页面会自动显示红色错误文本。
13.3 天气卡片展示条件
dart
if (_weather != null && !_isLoading)
Expanded(
child: Card(...),
),
这个条件保证加载时不会展示旧卡片,加载完成后再展示最新天气数据。
十四、天气卡片 UI 实现
14.1 Card 容器
dart
Expanded(
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(...),
),
)
天气结果区域使用 Expanded 占据剩余空间,内部 Card 通过阴影和圆角形成独立信息面板。
14.2 渐变 Container
dart
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
_getWeatherColor(_weather!.condition),
_getWeatherColor(_weather!.condition).withValues(alpha: 0.7),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(...),
)
容器圆角和 Card 圆角保持一致,渐变色来自当前天气状况。
14.3 信息层级
天气卡片内部使用 Column 垂直排列。
dart
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(...),
const SizedBox(height: 16),
Text(_weather!.city),
const SizedBox(height: 8),
Text('${_weather!.temperature}°C'),
Text(_weather!.condition),
const SizedBox(height: 32),
Row(...),
],
)
页面信息从上到下依次是天气图标、城市、温度、天气状况、湿度和风速。
14.4 字体层级
| 展示内容 | 字号 | 样式 |
|---|---|---|
| 城市名 | 32 | 加粗、白色 |
| 温度 | 64 | 细字重、白色 |
| 天气状况 | 24 | 白色半透明 |
| 湿度/风速值 | 18 | 加粗、白色 |
| 标签 | 14 | 白色半透明 |
温度是卡片中最大的文本,因为它是天气类应用的核心信息。
十五、湿度与风速信息块
15.1 _buildWeatherInfo 方法
湿度和风速使用同一个方法构建。
dart
Widget _buildWeatherInfo(IconData icon, String value, String label) {
return Column(
children: [
Icon(icon, color: Colors.white, size: 32),
const SizedBox(height: 4),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 14)),
],
);
}
这个方法接收图标、数值和标签,返回一个垂直排列的信息块。
15.2 调用方式
dart
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherInfo(Icons.water_drop, '${_weather!.humidity}%', 'Humidity'),
_buildWeatherInfo(Icons.air, '${_weather!.windSpeed} km/h', 'Wind'),
],
)
Row 使用 spaceEvenly 均分横向空间,让湿度和风速对称排列。
15.3 信息块结构
| 参数 | 湿度块 | 风速块 |
|---|---|---|
icon |
Icons.water_drop |
Icons.air |
value |
${humidity}% |
${windSpeed} km/h |
label |
Humidity |
Wind |
通过方法复用,卡片底部两个信息块的样式保持一致。
十六、OpenHarmony 适配边界
16.1 Flutter 层职责
当前天气应用的业务逻辑全部在 Flutter 层完成。
text
Flutter 层:
WeatherApp
WeatherData
WeatherHomePage
_WeatherHomePageState
TextField
Future.delayed
Card
LinearGradient
OpenHarmony 层:
AppScope
entry
EntryAbility
Index
resources
Flutter 层负责输入、状态、模拟查询、视觉映射和卡片展示;OpenHarmony 层负责应用启动、模块配置和资源承载。
16.2 平台侧文件作用
| 层级 | 文件 | 作用 |
|---|---|---|
| 应用级 | AppScope/app.json5 |
描述应用整体信息 |
| 模块级 | entry/src/main/module.json5 |
描述模块入口和页面 |
| Ability | EntryAbility.ets |
应用启动入口 |
| 页面 | Index.ets |
承载 Flutter 页面 |
| 资源 | resources/base |
图标、字符串和配置资源 |
| 插件 | GeneratedPluginRegistrant.ets |
插件注册入口 |
16.3 天气应用的适配特点
weather 当前不需要定位、网络、文件读写或后台权限。它主要验证 Flutter 输入控件、加载态、渐变卡片、图标和文本布局在 OpenHarmony 容器中的显示效果。
十七、测试与验证
17.1 交互验证路径
天气应用的核心交互可以按以下流程验证:
- 启动应用,页面标题显示
Weather。 - 输入框显示
Enter city name...。 - 输入
Beijing后点击搜索按钮。 - 页面出现加载指示器。
- 1 秒后展示北京天气卡片。
- 输入
Tokyo后按键盘提交。 - 页面展示
Rainy天气及蓝色卡片。 - 输入非内置城市,如
Shanghai。 - 页面展示由兜底逻辑生成的天气结果。
- 空输入时点击搜索,页面保持不变。
17.2 Widget 测试示例
下面的测试验证应用启动后存在输入框和搜索按钮。
dart
testWidgets('shows weather search input', (WidgetTester tester) async {
await tester.pumpWidget(const WeatherApp());
expect(find.text('Weather'), findsOneWidget);
expect(find.byIcon(Icons.location_city), findsOneWidget);
expect(find.byIcon(Icons.search), findsOneWidget);
});
17.3 搜索样例城市测试
下面的测试模拟搜索 Beijing。
dart
testWidgets('searches sample weather city', (WidgetTester tester) async {
await tester.pumpWidget(const WeatherApp());
await tester.enterText(find.byType(TextField), 'Beijing');
await tester.tap(find.byIcon(Icons.search));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(find.text('Beijing'), findsOneWidget);
expect(find.text('30°C'), findsOneWidget);
expect(find.text('Hazy'), findsOneWidget);
});
17.4 搜索兜底城市测试
dart
testWidgets('generates fallback weather for unknown city', (WidgetTester tester) async {
await tester.pumpWidget(const WeatherApp());
await tester.enterText(find.byType(TextField), 'Shanghai');
await tester.tap(find.byIcon(Icons.search));
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(find.text('Shanghai'), findsOneWidget);
expect(find.textContaining('°C'), findsWidgets);
});
这些测试覆盖了输入区、加载态、样例城市和兜底城市四个关键路径。
十八、常见问题解析
18.1 为什么使用 Future.delayed
当前项目没有真实网络请求,但天气查询本身通常是异步操作。使用 Future.delayed(const Duration(seconds: 1)) 可以模拟接口等待,让页面展示完整的加载状态。
dart
Future.delayed(const Duration(seconds: 1), () {
// 查询完成后更新天气数据
});
这样读者能看到搜索、加载、结果展示三个阶段。
18.2 为什么使用 firstWhere
firstWhere 可以从 _sampleWeather 中查找第一个匹配城市。
dart
_sampleWeather.firstWhere(
(w) => w.city.toLowerCase() == city.toLowerCase(),
orElse: () => WeatherData(...),
);
它和 orElse 搭配后,可以同时处理命中样例数据和未命中样例数据两种情况。
18.3 为什么城市匹配要转小写
用户输入的城市大小写不可控。使用 toLowerCase() 后,beijing、Beijing、BEIJING 都能命中同一条数据。
dart
w.city.toLowerCase() == city.toLowerCase()
这种处理可以提升输入容错。
18.4 为什么卡片颜色跟随天气变化
天气应用天然适合视觉映射:晴天偏橙色,雨天偏蓝色,多云偏灰色,有风偏青绿色。颜色变化能让用户在阅读文字之前先获得天气氛围。
18.5 为什么当前不需要权限
当前项目没有获取定位,也没有访问网络,只使用本地模拟数据。因此 OpenHarmony 侧不需要额外声明定位或网络权限。
十九、完整业务链路复盘
19.1 搜索链路
- 用户在输入框输入城市名。
- 点击搜索按钮或键盘提交。
_searchWeather()读取_cityController.text。trim()处理首尾空格。- 空输入直接返回。
- 非空输入进入加载状态。
Future.delayed模拟查询等待。firstWhere查找样例城市。- 未命中时
orElse生成兜底数据。 _weather更新为查询结果。_isLoading变为false。- 天气卡片展示最新结果。
19.2 映射链路
| 输入数据 | 映射方法 | UI 输出 |
|---|---|---|
condition |
_getWeatherIcon |
天气图标 |
condition |
_getWeatherColor |
渐变卡片颜色 |
city |
直接展示 | 城市标题 |
temperature |
字符串拼接 | ${temperature}°C |
humidity |
_buildWeatherInfo |
湿度信息块 |
windSpeed |
_buildWeatherInfo |
风速信息块 |
19.3 渲染链路
_weather != null && !_isLoading条件成立。- 页面构建
Expanded天气区域。 Card提供圆角和阴影。Container提供渐变背景。Column纵向排列图标、城市、温度、天气和指标。Row横向排列湿度和风速。
二十、核心源码总览
下面集中展示当前项目中最关键的数据、搜索和映射代码。
dart
class WeatherData {
final String city;
final int temperature;
final String condition;
final int humidity;
final double windSpeed;
WeatherData({
required this.city,
required this.temperature,
required this.condition,
required this.humidity,
required this.windSpeed,
});
}
void _searchWeather() {
final city = _cityController.text.trim();
if (city.isEmpty) return;
setState(() {
_isLoading = true;
_error = null;
});
Future.delayed(const Duration(seconds: 1), () {
final found = _sampleWeather.firstWhere(
(w) => w.city.toLowerCase() == city.toLowerCase(),
orElse: () => WeatherData(
city: city,
temperature: 20 + (city.length * 2) % 15,
condition: ['Sunny', 'Cloudy', 'Rainy', 'Clear', 'Windy'][city.length % 5],
humidity: 40 + (city.length * 7) % 50,
windSpeed: 5 + (city.length * 3) % 20,
),
);
setState(() {
_weather = found;
_isLoading = false;
});
});
}
这段代码体现了项目主干:输入城市、模拟加载、查找样例、生成兜底数据并刷新页面。
二十一、总结
weather 是一个非常适合学习 Flutter 数据驱动 UI 的天气查询项目。它通过 WeatherData 把城市、温度、天气状况、湿度和风速组织成结构化数据,再通过输入框、加载状态、样例数据、兜底生成、图标映射、颜色映射和渐变卡片完成完整页面体验。
从源码结构看,项目的关键点集中在四条线:TextEditingController 负责输入,Future.delayed 模拟异步查询,WeatherData 承载查询结果,_getWeatherIcon 与 _getWeatherColor 负责把天气状况转化为视觉表现。页面渲染则通过 Card、Container、LinearGradient、Column 和 Row 完成层次化展示。
从 OpenHarmony 适配角度看,当前项目业务逻辑保持在 Flutter 层,ohos 目录负责平台工程承载。天气页面包含输入、加载、卡片、图标、渐变和动态数据切换,能很好地验证 Flutter 页面在 OpenHarmony 容器中的基础交互与展示效果。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续创作 Flutter 与 OpenHarmony 实战内容的动力!
相关资源
- Flutter 官方文档:https://docs.flutter.dev/
- Flutter Widget 目录:https://docs.flutter.dev/ui/widgets
- Dart
FutureAPI:https://api.dart.dev/stable/dart-async/Future-class.html - OpenHarmony 官网:https://www.openharmony.cn/
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net