Flutter+OpenHarmony 实战:weather 天气查询应用

目录

    • 前言
    • 一、项目背景与功能定位
      • [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 工程承载上。

本文围绕当前项目真实源码展开,重点拆解:

  1. WeatherApp 如何配置应用入口和浅蓝色主题。
  2. WeatherData 如何表达城市、温度、天气、湿度和风速。
  3. _searchWeather() 如何处理输入、加载状态、样例匹配和兜底数据。
  4. _getWeatherIcon()_getWeatherColor() 如何把天气状况映射到视觉元素。
  5. 渐变天气卡片如何展示图标、温度、湿度和风速。
  6. 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'),
    );
  }
}

这里有三个关键信息:

  1. 应用标题是 Weather
  2. 主题色使用 Colors.lightBlue,与天气类应用气质一致。
  3. 首页是 WeatherHomePage,天气搜索状态从首页 State 中维护。

4.3 Material 3 主题

dart 复制代码
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue),
  useMaterial3: true,
),

useMaterial3: true 会影响 AppBarTextFieldElevatedButtonCard 等组件的默认视觉效果。当前项目在天气卡片中进一步使用渐变色,让不同天气状态具有更直观的识别度。


五、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(...),
);

这里使用大小写不敏感匹配。输入 beijingBeijingBEIJING 都能命中 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 交互验证路径

天气应用的核心交互可以按以下流程验证:

  1. 启动应用,页面标题显示 Weather
  2. 输入框显示 Enter city name...
  3. 输入 Beijing 后点击搜索按钮。
  4. 页面出现加载指示器。
  5. 1 秒后展示北京天气卡片。
  6. 输入 Tokyo 后按键盘提交。
  7. 页面展示 Rainy 天气及蓝色卡片。
  8. 输入非内置城市,如 Shanghai
  9. 页面展示由兜底逻辑生成的天气结果。
  10. 空输入时点击搜索,页面保持不变。

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() 后,beijingBeijingBEIJING 都能命中同一条数据。

dart 复制代码
w.city.toLowerCase() == city.toLowerCase()

这种处理可以提升输入容错。

18.4 为什么卡片颜色跟随天气变化

天气应用天然适合视觉映射:晴天偏橙色,雨天偏蓝色,多云偏灰色,有风偏青绿色。颜色变化能让用户在阅读文字之前先获得天气氛围。

18.5 为什么当前不需要权限

当前项目没有获取定位,也没有访问网络,只使用本地模拟数据。因此 OpenHarmony 侧不需要额外声明定位或网络权限。


十九、完整业务链路复盘

19.1 搜索链路

  1. 用户在输入框输入城市名。
  2. 点击搜索按钮或键盘提交。
  3. _searchWeather() 读取 _cityController.text
  4. trim() 处理首尾空格。
  5. 空输入直接返回。
  6. 非空输入进入加载状态。
  7. Future.delayed 模拟查询等待。
  8. firstWhere 查找样例城市。
  9. 未命中时 orElse 生成兜底数据。
  10. _weather 更新为查询结果。
  11. _isLoading 变为 false
  12. 天气卡片展示最新结果。

19.2 映射链路

输入数据 映射方法 UI 输出
condition _getWeatherIcon 天气图标
condition _getWeatherColor 渐变卡片颜色
city 直接展示 城市标题
temperature 字符串拼接 ${temperature}°C
humidity _buildWeatherInfo 湿度信息块
windSpeed _buildWeatherInfo 风速信息块

19.3 渲染链路

  1. _weather != null && !_isLoading 条件成立。
  2. 页面构建 Expanded 天气区域。
  3. Card 提供圆角和阴影。
  4. Container 提供渐变背景。
  5. Column 纵向排列图标、城市、温度、天气和指标。
  6. 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 负责把天气状况转化为视觉表现。页面渲染则通过 CardContainerLinearGradientColumnRow 完成层次化展示。

从 OpenHarmony 适配角度看,当前项目业务逻辑保持在 Flutter 层,ohos 目录负责平台工程承载。天气页面包含输入、加载、卡片、图标、渐变和动态数据切换,能很好地验证 Flutter 页面在 OpenHarmony 容器中的基础交互与展示效果。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续创作 Flutter 与 OpenHarmony 实战内容的动力!


相关资源

相关推荐
yuegu7771 小时前
HarmonyOS应用<节气通>开发第1篇:启动页开发——留下第一印象的2秒
harmonyos
川石课堂软件测试1 小时前
零基础小白如何学习自动化测试
python·功能测试·学习·测试工具·jmeter·压力测试·harmonyos
韩曙亮1 小时前
【错误记录】flutter pub get 执行报错 ( 打开 Windows 开发者模式 )
windows·flutter
Swift社区2 小时前
OpenHarmony鸿蒙PC平台移植 gifsicle:CC++ 三方库适配实践(Lycium tpc_c_cplusplus)
c语言·c++·harmonyos
川石课堂软件测试2 小时前
作为一名测试工程师如何学习Kubernetes(k8s)技能
学习·测试工具·容器·职场和发展·kubernetes·测试用例·harmonyos
yuegu7773 小时前
HarmonyOS应用<节气通>开发第4篇:TabBar导航实现
华为·harmonyos
阿钱真强道3 小时前
25 鸿蒙LiteOS GPIO轮询模式实战教程:电平读取与上升沿检测
嵌入式·harmonyos·liteos·开源鸿蒙·瑞芯微·rk2206
G_dou_3 小时前
Flutter+OpenHarmony实战:flashlight】手电筒项目
flutter·harmonyos
爱吃大芒果4 小时前
鸿蒙 ArkUI 架构蓝图:MoodLite 的 UI 渲染与数据逻辑解耦实践
ui·架构·harmonyos