Flutter 三方库 鸿蒙天气日历助手项目实践:基于 AP12 与鸿蒙 6.0+ 的跨端开发案例

一、文章简介

本文是一篇面向 Flutter 跨端开发场景的 Flutter + 三方库 + 鸿蒙 项目实践文档。文中以 天气日历助手 为案例,完整展示一个天气信息展示项目的实现过程。这个项目可以完成以下功能:

  • 展示今日日期和当前城市
  • 通过三方库获取网络天气数据
  • 使用卡片形式展示天气信息
  • 支持下拉刷新
  • 用于说明 Flutter 跨端开发在鸿蒙设备上的基础实现方式

这篇文章的目标不是追求复杂业务,而是帮助鸿蒙开发者快速理解:

  1. Flutter 项目怎么创建
  2. 三方库怎么接入
  3. 页面怎么组织
  4. 网络请求怎么写
  5. 数据模型怎么设计
  6. 如何把一个 Flutter 应用作为跨端项目思路迁移到鸿蒙场景

二、案例名称

项目名称:天气日历助手

这个项目的实践意义主要体现在以下三个方面:

  • 页面结构简单,便于理解 Flutter 组件化开发
  • 会用到常见三方库,具有真实项目意义
  • 同时覆盖 UI、网络请求、状态更新、列表渲染等核心知识点

三、为什么选择这个案例

在 Flutter 入门阶段,开发者通常会遇到以下两个问题:

  • 只会写静态页面,不会做实际功能
  • 不知道三方库在项目中怎么接入和使用

而对于鸿蒙开发者来说,还多一个问题:

  • 已经会原生开发,但不知道 Flutter 跨端项目的基本结构

所以这里选择“天气日历助手”作为案例,能够让你一次练到这些核心点:

  • http:发送网络请求
  • intl:处理日期格式化
  • pull_to_refresh:实现下拉刷新

这三个都是 Flutter 项目中较常见的三方库,案例规模适中、展示效果直观,能够覆盖基础开发中的关键能力点。


四、项目效果说明

本项目完成后,页面会显示:

  • 顶部标题:天气日历助手
  • 当前日期
  • 当前城市名称
  • 今日气温
  • 天气描述
  • 风力信息
  • 一个“刷新天气”按钮
  • 支持列表下拉刷新

该页面属于标准的信息展示类界面,用于说明 Flutter 在鸿蒙设备上的基础页面构建与数据交互流程。


五、环境准备

在开始之前,你需要准备以下环境。本文默认的目标运行环境为:

  • HarmonyOS 6.0 及以上版本
  • API Version 12,也就是 AP120,或更高版本 SDK

也就是说,本文案例不是面向旧版本鸿蒙环境,而是面向 鸿蒙 6.0+ / AP120+ 的开发与实践场景。

1. 安装 Flutter SDK

先确认本机已经安装 Flutter:

flutter --version

如果终端能够输出 Flutter 版本号,说明安装成功。

2. 配置开发工具

准备支持 Flutter 与 Dart 开发的编辑环境,并确保命令行可以正常执行 flutter 相关命令。

3. 配置鸿蒙 6.0+ 开发环境

如果你要把这个项目放到鸿蒙场景中实践,建议明确以下基础条件:

  • DevEco Studio 中安装 AP120 或以上版本 SDK
  • 目标设备系统版本为 HarmonyOS 6.0 或以上
  • 已具备 Flutter 对接鸿蒙的运行或适配环境

这样做的原因是:不同鸿蒙版本在 SDK 能力、工程配置和运行支持上会有差异。本文为了避免环境混乱,统一以 AP120+ 与鸿蒙 6.0+ 作为实践前提。

4. 准备鸿蒙设备或模拟环境

如果你要把这个项目放到鸿蒙场景中实践,通常需要准备:

  • 鸿蒙 6.0 及以上设备
  • 或者对应的运行环境
  • 以及你当前所使用的 Flutter 鸿蒙适配方案

说明:不同阶段的 Flutter 对鸿蒙的适配方式可能不同。本文重点放在 Flutter 项目开发实践本身,也就是页面、逻辑、三方库接入和代码组织。运行本文案例时,请确保你的鸿蒙工程 SDK 版本不低于 AP120,系统版本不低于 HarmonyOS 6.0


六、创建 Flutter 项目

打开终端,执行下面命令:

flutter create weather_mate

进入项目目录:

cd weather_mate

运行项目:

flutter run

如果看到默认计数器页面,说明项目创建成功。


七、添加三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中加入以下内容:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1
  intl: ^0.19.0
  pull_to_refresh: ^2.0.0

三方库作用说明

1. http

用于发送 HTTP 网络请求,从天气接口获取数据。

2. intl

用于格式化日期,比如把当前时间显示成 2026年04月08日 星期三 这种更友好的格式。

3. pull_to_refresh

用于给页面增加下拉刷新能力。

添加完成后执行:

flutter pub get

这一步会下载依赖包。


八、项目目录设计

为了让代码结构更清晰,我们可以把项目拆分成下面几个部分:

lib/
├── main.dart
├── models/
│   └── weather_info.dart
├── pages/
│   └── weather_home_page.dart
└── services/
    └── weather_service.dart

为什么这样拆分

  • main.dart:应用入口
  • models:数据模型层
  • pages:页面层
  • services:服务层,专门处理网络请求

这是一种较为基础但实用的 Flutter 项目结构,能够较好地支持页面、模型和服务逻辑的拆分。


九、天气接口说明

本项目采用标准 HTTP JSON 接口方式获取天气数据。为了保持文档的通用性,下面使用占位形式的业务接口路径进行说明:

/api/weather?city=Beijing

接口返回的数据格式约定如下:

{
  "current_condition": [
    {
      "temp_C": "26",
      "weatherDesc": [
        {
          "value": "Sunny"
        }
      ],
      "windspeedKmph": "12"
    }
  ]
}

在实际项目中,只要接口能够返回结构相近的 JSON 数据,就可以复用本文中的解析逻辑。


十、开始写代码

下面我们一步一步完成整个项目。


十一、编写数据模型

新建文件:lib/models/weather_info.dart

class WeatherInfo {
  final String city;
  final String temperature;
  final String weatherDesc;
  final String windSpeed;

  WeatherInfo({
    required this.city,
    required this.temperature,
    required this.weatherDesc,
    required this.windSpeed,
  });

  // 工厂构造函数:把接口返回的 JSON 数据解析成 WeatherInfo 对象
  factory WeatherInfo.fromJson(Map<String, dynamic> json, String cityName) {
    final currentCondition = json['current_condition'][0];

    return WeatherInfo(
      city: cityName,
      temperature: currentCondition['temp_C'] ?? '0',
      weatherDesc: currentCondition['weatherDesc'][0]['value'] ?? '未知天气',
      windSpeed: currentCondition['windspeedKmph'] ?? '0',
    );
  }
}

代码解释

1. WeatherInfo 是什么

这是一个数据模型类,用来统一保存天气信息。

2. 为什么要建模型类

因为接口返回的是原始 JSON,如果直接在页面里乱写取值逻辑,代码会很乱。把数据先整理成一个对象,页面就会更清晰。

3. factory WeatherInfo.fromJson

这是一个工厂构造方法,用来把接口返回的数据转换成 Dart 对象。


十二、编写网络请求服务

新建文件:lib/services/weather_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/weather_info.dart';

class WeatherService {
  // 获取指定城市的天气信息
  Future<WeatherInfo> fetchWeather(String city) async {
    final url = Uri.parse('https://your-server-address/api/weather?city=$city');

    // 发送 GET 请求
    final response = await http.get(url);

    // 判断请求是否成功
    if (response.statusCode == 200) {
      final Map<String, dynamic> jsonData = json.decode(response.body);

      // 把 JSON 数据转换成 WeatherInfo 对象
      return WeatherInfo.fromJson(jsonData, city);
    } else {
      // 如果接口请求失败,主动抛出异常,方便页面层捕获
      throw Exception('天气数据获取失败');
    }
  }
}

代码解释

1. http.get(url)

用于发起网络请求。

2. json.decode(response.body)

把字符串类型的 JSON 转成 Dart 中的 Map 对象。

3. throw Exception(...)

表示请求失败时主动抛出错误,后续页面里可以捕获并提示用户。


十三、编写主页面

新建文件:lib/pages/weather_home_page.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../models/weather_info.dart';
import '../services/weather_service.dart';

class WeatherHomePage extends StatefulWidget {
  const WeatherHomePage({super.key});

  
  State<WeatherHomePage> createState() => _WeatherHomePageState();
}

class _WeatherHomePageState extends State<WeatherHomePage> {
  final WeatherService _weatherService = WeatherService();
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  WeatherInfo? weatherInfo;
  bool isLoading = true;
  String errorText = '';
  final String cityName = 'Beijing';

  
  void initState() {
    super.initState();
    _loadWeather();
  }

  // 加载天气数据
  Future<void> _loadWeather() async {
    try {
      final data = await _weatherService.fetchWeather(cityName);
      setState(() {
        weatherInfo = data;
        isLoading = false;
        errorText = '';
      });
    } catch (e) {
      setState(() {
        isLoading = false;
        errorText = '加载失败:$e';
      });
    }
  }

  // 下拉刷新逻辑
  Future<void> _onRefresh() async {
    await _loadWeather();
    _refreshController.refreshCompleted();
  }

  
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    // 使用 intl 三方库格式化日期
    final String currentDate =
        DateFormat('yyyy年MM月dd日 EEEE', 'zh_CN').format(DateTime.now());

    return Scaffold(
      appBar: AppBar(
        title: const Text('天气日历助手'),
        centerTitle: true,
        backgroundColor: Colors.lightBlue,
      ),
      body: SmartRefresher(
        controller: _refreshController,
        onRefresh: _onRefresh,
        enablePullDown: true,
        child: _buildBody(currentDate),
      ),
    );
  }

  Widget _buildBody(String currentDate) {
    if (isLoading) {
      // 数据加载中时显示进度条
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (errorText.isNotEmpty) {
      // 加载失败时显示错误信息
      return Center(
        child: Text(
          errorText,
          style: const TextStyle(fontSize: 16, color: Colors.red),
        ),
      );
    }

    if (weatherInfo == null) {
      // 理论上的兜底分支,避免空对象导致页面崩溃
      return const Center(
        child: Text('暂无天气数据'),
      );
    }

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Card(
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '今日天气',
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 12),
                Text(
                  '日期:$currentDate',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 8),
                Text(
                  '城市:${weatherInfo!.city}',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 8),
                Text(
                  '温度:${weatherInfo!.temperature}℃',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 8),
                Text(
                  '天气:${weatherInfo!.weatherDesc}',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 8),
                Text(
                  '风速:${weatherInfo!.windSpeed} km/h',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 20),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: _loadWeather,
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.lightBlue,
                      foregroundColor: Colors.white,
                      padding: const EdgeInsets.symmetric(vertical: 14),
                    ),
                    child: const Text('刷新天气'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

代码解释

1. 为什么使用 StatefulWidget

因为天气数据是动态加载的,页面内容会随着请求结果变化,所以这里使用有状态组件。

2. initState() 做了什么

页面一打开就调用 _loadWeather(),自动获取天气数据。

3. setState() 的作用

当数据请求完成后,调用 setState() 可以通知 Flutter 重新构建界面。

4. SmartRefresher

这是 pull_to_refresh 三方库提供的组件,用于实现下拉刷新功能。

5. _buildBody()

把页面主体拆成独立方法,能让 build() 更简洁。


十四、修改应用入口文件

打开 lib/main.dart,替换成下面代码:

import 'package:flutter/material.dart';
import 'pages/weather_home_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '天气日历助手',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const WeatherHomePage(),
    );
  }
}

代码解释

1. runApp(const MyApp())

这是 Flutter 应用启动入口。

2. MaterialApp

它是一个 Material Design 风格应用的根组件,负责应用主题、路由、标题等配置。

3. home: const WeatherHomePage()

表示应用启动后默认进入天气首页。


十五、完整代码汇总

为了便于统一查看项目结构,这里对关键文件进行一次汇总。

1. lib/main.dart

import 'package:flutter/material.dart';
import 'pages/weather_home_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '天气日历助手',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const WeatherHomePage(),
    );
  }
}

2. lib/models/weather_info.dart

class WeatherInfo {
  final String city;
  final String temperature;
  final String weatherDesc;
  final String windSpeed;

  WeatherInfo({
    required this.city,
    required this.temperature,
    required this.weatherDesc,
    required this.windSpeed,
  });

  factory WeatherInfo.fromJson(Map<String, dynamic> json, String cityName) {
    final currentCondition = json['current_condition'][0];

    return WeatherInfo(
      city: cityName,
      temperature: currentCondition['temp_C'] ?? '0',
      weatherDesc: currentCondition['weatherDesc'][0]['value'] ?? '未知天气',
      windSpeed: currentCondition['windspeedKmph'] ?? '0',
    );
  }
}

3. lib/services/weather_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/weather_info.dart';

class WeatherService {
  Future<WeatherInfo> fetchWeather(String city) async {
    final url = Uri.parse('https://your-server-address/api/weather?city=$city');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final Map<String, dynamic> jsonData = json.decode(response.body);
      return WeatherInfo.fromJson(jsonData, city);
    } else {
      throw Exception('天气数据获取失败');
    }
  }
}

4. lib/pages/weather_home_page.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../models/weather_info.dart';
import '../services/weather_service.dart';

class WeatherHomePage extends StatefulWidget {
  const WeatherHomePage({super.key});

  
  State<WeatherHomePage> createState() => _WeatherHomePageState();
}

class _WeatherHomePageState extends State<WeatherHomePage> {
  final WeatherService _weatherService = WeatherService();
  final RefreshController _refreshController =
      RefreshController(initialRefresh: false);

  WeatherInfo? weatherInfo;
  bool isLoading = true;
  String errorText = '';
  final String cityName = 'Beijing';

  
  void initState() {
    super.initState();
    _loadWeather();
  }

  Future<void> _loadWeather() async {
    try {
      final data = await _weatherService.fetchWeather(cityName);
      setState(() {
        weatherInfo = data;
        isLoading = false;
        errorText = '';
      });
    } catch (e) {
      setState(() {
        isLoading = false;
        errorText = '加载失败:$e';
      });
    }
  }

  Future<void> _onRefresh() async {
    await _loadWeather();
    _refreshController.refreshCompleted();
  }

  
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final String currentDate =
        DateFormat('yyyy年MM月dd日 EEEE', 'zh_CN').format(DateTime.now());

    return Scaffold(
      appBar: AppBar(
        title: const Text('天气日历助手'),
        centerTitle: true,
        backgroundColor: Colors.lightBlue,
      ),
      body: SmartRefresher(
        controller: _refreshController,
        onRefresh: _onRefresh,
        enablePullDown: true,
        child: _buildBody(currentDate),
      ),
    );
  }

  Widget _buildBody(String currentDate) {
    if (isLoading) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (errorText.isNotEmpty) {
      return Center(
        child: Text(
          errorText,
          style: const TextStyle(fontSize: 16, color: Colors.red),
        ),
      );
    }

    if (weatherInfo == null) {
      return const Center(
        child: Text('暂无天气数据'),
      );
    }

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Card(
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '今日天气',
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 12),
                Text('日期:$currentDate', style: const TextStyle(fontSize: 16)),
                const SizedBox(height: 8),
                Text('城市:${weatherInfo!.city}',
                    style: const TextStyle(fontSize: 16)),
                const SizedBox(height: 8),
                Text('温度:${weatherInfo!.temperature}℃',
                    style: const TextStyle(fontSize: 16)),
                const SizedBox(height: 8),
                Text('天气:${weatherInfo!.weatherDesc}',
                    style: const TextStyle(fontSize: 16)),
                const SizedBox(height: 8),
                Text('风速:${weatherInfo!.windSpeed} km/h',
                    style: const TextStyle(fontSize: 16)),
                const SizedBox(height: 20),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: _loadWeather,
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.lightBlue,
                      foregroundColor: Colors.white,
                      padding: const EdgeInsets.symmetric(vertical: 14),
                    ),
                    child: const Text('刷新天气'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

十六、如何运行项目

1. 拉取依赖

flutter pub get

2. 启动项目

flutter run

3. 在鸿蒙环境中运行

你可以按自己的 Flutter 鸿蒙适配流程进行以下操作:

  1. 准备鸿蒙运行环境
  2. 导入 Flutter 项目
  3. 完成平台适配
  4. 部署到鸿蒙设备
  5. 观察页面显示和网络请求是否正常

本文案例的重点是:先掌握 Flutter 跨端项目的开发过程,再映射到 AP120+、鸿蒙 6.0+ 的设备运行环境中。


十七、项目中学到了什么

完成这个案例之后,你已经掌握了以下内容:

  • 如何创建 Flutter 项目
  • 如何接入三方库
  • 如何发送网络请求
  • 如何解析 JSON 数据
  • 如何封装数据模型
  • 如何使用 StatefulWidget 管理页面状态
  • 如何实现下拉刷新
  • 如何组织一个简单的 Flutter 项目目录

这些能力已经足够支撑你继续做更复杂的 Flutter 跨端项目。


十八、项目优化方向

在当前实现基础上,项目还可以继续向以下方向扩展:

1. 增加城市切换功能

可以让用户输入城市名,动态查询不同地区天气。

2. 增加天气图标

可以根据天气状态显示晴天、阴天、雨天等图标。

3. 增加未来天气预报

除了显示今天,还可以显示未来 3 天或 7 天数据。

4. 接入本地存储

可以把上一次查询的城市保存下来,下次打开直接读取。

5. 做成更标准的跨端项目

后续可以继续研究:

  • Flutter 与鸿蒙平台交互
  • 插件适配
  • 原生能力调用
  • 跨端 UI 一致性处理

十九、项目总结

本文通过“天气日历助手”这一具体案例,完成了一个 Flutter 跨端项目的基础实践。项目接入了 httpintlpull_to_refresh 等三方库,实现了天气查询、日期展示、状态刷新等功能。本文以 AP120 及以上 SDK、HarmonyOS 6.0 及以上系统版本为目标环境,完整展示了从项目创建、依赖接入、网络请求、数据建模到页面渲染的实现过程。


二十、全文结论

对于鸿蒙开发场景而言,Flutter 的学习和实践应当落在具体项目上,而不仅仅停留在概念层面。本文的“天气日历助手”项目虽然体量较小,但已经覆盖了 Flutter 入门阶段较为核心的知识点。

基于这一案例,可以进一步扩展到待办清单、新闻阅读器、记账应用、地图定位工具等更复杂的跨端项目,并逐步深入到 Flutter 与鸿蒙平台交互、插件适配和原生能力调用等方向。


二十一、文档标题

Flutter 三方库 鸿蒙天气日历助手项目实践:基于 AP120 与鸿蒙 6.0+ 的跨端开发案例

Logo

一站式 AI 云服务平台

更多推荐