欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Flutter鸿蒙开发实践案例:集成三方库实现智能天气助手

一、项目概述

1.1 项目背景

随着鸿蒙(HarmonyOS)生态的快速发展,Flutter作为优秀的跨平台开发框架,为开发者提供了在鸿蒙平台上构建应用的便捷途径。本案例将详细介绍如何使用Flutter结合多个三方库,开发一个功能完整的智能天气助手应用。

1.2 项目目标

  • 掌握Flutter在鸿蒙平台上的开发流程
  • 学习集成和使用多个Flutter三方库
  • 实现天气查询、定位、缓存等核心功能
  • 理解跨平台开发的最佳实践

1.3 技术栈

  • Flutter: 3.16+ (支持鸿蒙)
  • 鸿蒙: HarmonyOS 6.0+
  • 鸿蒙SDK: API 20+
  • http: ^1.2.0 - 网络请求库
  • geolocator: ^10.1.0 - 地理定位库
  • shared_preferences: ^2.2.2 - 本地数据持久化
  • flutter_spinkit: ^5.2.0 - 加载动画库
  • intl: ^0.18.1 - 国际化支持

二、项目准备

2.1 环境配置

2.1.1 安装Flutter SDK
# 下载Flutter SDK (确保版本支持鸿蒙)
# https://docs.flutter.dev/get-started/install

# 配置环境变量
# Windows: 将 flutter/bin 添加到 PATH
# 验证安装
flutter doctor
2.1.2 配置鸿蒙开发环境
# 1. 安装DevEco Studio 5.0+
# https://developer.harmonyos.com/cn/develop/deveco-studio

# 2. 安装鸿蒙SDK (API 20+)
# 在DevEco Studio中安装HarmonyOS SDK,确保API Level >= 20

# 3. 配置Flutter鸿蒙支持
flutter config --enable-harmony-os

# 4. 验证鸿蒙SDK版本
flutter doctor -v

2.2 创建项目

# 创建Flutter项目
flutter create weather_app_harmony

# 进入项目目录
cd weather_app_harmony

# 查看项目结构
tree /F

2.3 配置pubspec.yaml

name: weather_app_harmony
description: Flutter鸿蒙智能天气助手应用
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # 网络请求库 - 用于调用天气API
  http: ^1.2.0

  # 地理定位库 - 获取用户当前位置
  geolocator: ^10.1.0

  # 本地缓存库 - 存储用户偏好和天气数据
  shared_preferences: ^2.2.2

  # 加载动画库 - 美化加载状态
  flutter_spinkit: ^5.2.0

  # 国际化库 - 支持多语言
  intl: ^0.18.1

  # UI组件库 - 提供美观的卡片和列表
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

# 鸿蒙平台配置
harmony:
  # 指定鸿蒙SDK最低API版本
  compileSdkVersion: 20
  # 指定目标鸿蒙系统版本
  targetSdkVersion: 20

三、项目架构设计

3.1 目录结构

lib/
├── main.dart                 # 应用入口
├── models/                   # 数据模型
│   └── weather_model.dart   # 天气数据模型
├── services/                 # 业务服务
│   ├── weather_service.dart  # 天气API服务
│   └── location_service.dart # 定位服务
├── repositories/             # 数据仓库层
│   └── weather_repository.dart
├── widgets/                  # 自定义组件
│   ├── weather_card.dart    # 天气卡片组件
│   └── loading_widget.dart  # 加载动画组件
└── screens/                  # 页面
    └── home_screen.dart     # 主页面

3.2 架构模式

采用MVVM架构模式:

  • Model: 数据模型层,定义数据结构
  • View: 视图层,负责UI展示
  • ViewModel: 视图模型层,处理业务逻辑

四、核心功能实现

4.0 鸿蒙平台配置

在鸿蒙平台上运行需要配置权限和模块信息。在 harmony/entry/src/main/module.json5 中添加以下配置:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "天气应用主模块",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "default",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "天气应用入口页面",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "需要网络权限获取天气数据",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "需要获取位置信息提供本地天气",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

重要说明:

  • ohos.permission.INTERNET: 网络权限,用于调用天气API
  • ohos.permission.APPROXIMATELY_LOCATION: 模糊定位权限,API 20+推荐使用
  • 设备类型支持手机和平板
  • 最低兼容API版本为20

4.1 数据模型层

创建 lib/models/weather_model.dart:

/// 天气数据模型
class WeatherModel {
  final String cityName;
  final double temperature;
  final String description;
  final int humidity;
  final double windSpeed;
  final String iconUrl;

  WeatherModel({
    required this.cityName,
    required this.temperature,
    required this.description,
    required this.humidity,
    required this.windSpeed,
    required this.iconUrl,
  });

  factory WeatherModel.fromJson(Map<String, dynamic> json) {
    return WeatherModel(
      cityName: json['name'],
      temperature: json['main']['temp'].toDouble(),
      description: json['weather'][0]['description'],
      humidity: json['main']['humidity'],
      windSpeed: json['wind']['speed'].toDouble(),
      iconUrl: 'https://openweathermap.org/img/wn/${json['weather'][0]['icon']}@2x.png',
    );
  }

  String get formattedTemperature => '${temperature.round()}°C';
}

4.2 服务层

4.2.1 天气API服务 (使用http库)
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/weather_model.dart';

class WeatherService {
  static const String _apiKey = 'YOUR_API_KEY';
  static const String _baseUrl = 'https://api.openweathermap.org/data/2.5';

  Future<WeatherModel> getWeatherByCity(String cityName) async {
    final url = Uri.parse('$_baseUrl/weather?q=$cityName&appid=$_apiKey&units=metric');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      return WeatherModel.fromJson(json.decode(response.body));
    }
    throw Exception('获取天气失败');
  }
}
4.2.2 定位服务 (使用geolocator库)
import 'package:geolocator/geolocator.dart';

class LocationService {
  Future<Position> getCurrentLocation() async {
    bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
    LocationPermission permission = await Geolocator.checkPermission();

    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
    }

    // 鸿蒙API 20+支持模糊定位,优先使用模糊定位保护用户隐私
    return await Geolocator.getCurrentPosition(
      desiredAccuracy: LocationAccuracy.medium, // 使用中等精度,兼容鸿蒙模糊定位
      timeLimit: const Duration(seconds: 10), // 设置超时时间
    );
  }
}

4.3 数据仓库层 (使用shared_preferences库)

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/weather_model.dart';
import '../services/weather_service.dart';
import '../services/location_service.dart';

class WeatherRepository {
  final WeatherService _weatherService = WeatherService();
  final LocationService _locationService = LocationService();

  Future<WeatherModel> getWeather({String? cityName}) async {
    // 尝试从缓存读取
    final prefs = await SharedPreferences.getInstance();
    final cached = prefs.getString('cached_weather');
    if (cached != null) {
      return WeatherModel.fromJson(json.decode(cached));
    }

    // 获取新数据
    WeatherModel weather;
    if (cityName != null) {
      weather = await _weatherService.getWeatherByCity(cityName);
    } else {
      final position = await _locationService.getCurrentLocation();
      weather = await _weatherService.getWeatherByCity('Beijing'); // 简化处理
    }

    // 缓存数据
    await prefs.setString('cached_weather', json.encode({
      'name': weather.cityName,
      'main': {'temp': weather.temperature, 'humidity': weather.humidity},
      'weather': [{'description': weather.description, 'icon': '01d'}],
      'wind': {'speed': weather.windSpeed}
    }));

    return weather;
  }
}

4.4 UI组件

4.4.1 加载动画 (使用flutter_spinkit库)
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';

class LoadingWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: SpinKitFadingCircle(
        color: Theme.of(context).primaryColor,
        size: 50.0,
      ),
    );
  }
}
4.4.2 天气卡片
import 'package:flutter/material.dart';
import '../models/weather_model.dart';

class WeatherCard extends StatelessWidget {
  final WeatherModel weather;

  const WeatherCard({required this.weather});

  
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(weather.cityName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            Text(weather.formattedTemperature, style: const TextStyle(fontSize: 48, color: Colors.blue)),
            Text(weather.description, style: const TextStyle(fontSize: 18, color: Colors.grey)),
          ],
        ),
      ),
    );
  }
}

4.5 主页面

import 'package:flutter/material.dart';
import '../repositories/weather_repository.dart';
import '../widgets/weather_card.dart';
import '../widgets/loading_widget.dart';

class HomeScreen extends StatefulWidget {
  
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final WeatherRepository _repository = WeatherRepository();
  bool _isLoading = false;
  String? _errorMessage;

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

  Future<void> _loadWeather() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      await _repository.getWeather();
      setState(() => _isLoading = false);
    } catch (e) {
      setState(() {
        _isLoading = false;
        _errorMessage = e.toString();
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('智能天气助手')),
      body: _isLoading
          ? const LoadingWidget()
          : _errorMessage != null
              ? Center(child: Text('加载失败: $_errorMessage'))
              : const Center(child: Text('天气数据加载完成')),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadWeather,
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

4.6 应用入口

import 'package:flutter/material.dart';
import 'screens/home_screen.dart';

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

class WeatherApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '智能天气助手',
      theme: ThemeData(useMaterial3: true),
      home: const HomeScreen(),
    );
  }
}

五、运行和测试

5.1 安装依赖

# 在项目根目录执行
flutter pub get

5.2 运行应用

5.2.1 在鸿蒙模拟器上运行
# 启动鸿蒙模拟器 (确保系统版本 >= 6.0)
# 在DevEco Studio中创建并启动模拟器

# 然后运行应用
flutter run -d harmony

# 查看可用的鸿蒙设备
flutter devices
5.2.2 在鸿蒙真机上运行
# 连接鸿蒙设备 (确保系统版本 >= 6.0)
# 通过USB连接设备,并在设备上开启USB调试

# 验证设备连接
flutter devices

# 运行应用
flutter run -d <device_id>

# 构建HAP包
flutter build harmony --release

5.3 测试功能

  1. 定位功能测试

    • 应用启动后自动获取当前位置天气
    • 点击浮动按钮刷新位置
  2. 城市搜索测试

    • 点击搜索按钮
    • 输入城市名称(如"北京"、“上海”)
    • 查看对应城市的天气信息
  3. 缓存功能测试

    • 关闭应用后重新打开
    • 查看是否显示缓存的天气数据
    • 30分钟后缓存自动失效
  4. 错误处理测试

    • 断开网络连接
    • 查看错误提示和缓存数据展示
  5. UI交互测试

    • 下拉刷新
    • 点击刷新按钮
    • 查看加载动画效果

六、项目总结

6.1 技术亮点

  1. 三方库集成

    • http: 简洁的网络请求实现
    • geolocator: 跨平台定位功能
    • shared_preferences: 轻量级本地存储
    • flutter_spinkit: 优雅的加载动画
  2. 架构设计

    • MVVM架构模式,代码结构清晰
    • 分层设计,职责明确
    • 便于维护和扩展
  3. 用户体验

    • Material 3设计规范
    • 流畅的动画效果
    • 完善的错误处理
  4. 性能优化

    • 数据缓存机制
    • 异步加载
    • 状态管理优化

6.2 鸿蒙平台适配要点

  1. 系统版本要求

    • 最低支持HarmonyOS 6.0+
    • 使用API 20+ SDK
    • 兼容鸿蒙Next系统特性
  2. 权限配置

    • 在鸿蒙平台的module.json5中配置定位权限
    • 使用模糊定位权限(APPROXIMATELY_LOCATION)保护用户隐私
    • 处理权限请求流程
  3. 网络配置

    • 配置鸿蒙网络权限
    • 处理HTTPS证书
    • 支持鸿蒙网络安全策略
  4. 定位适配

    • 使用中等精度定位,兼容鸿蒙模糊定位
    • 设置合理的超时时间
    • 处理定位服务不可用的情况
  5. UI适配

    • 鸿蒙系统的UI规范
    • 字体和图标适配
    • 支持鸿蒙设备多形态

6.3 后续优化方向

  1. 功能扩展

    • 添加未来几天的天气预报
    • 支持多城市管理
    • 添加天气预警功能
  2. 性能优化

    • 使用Provider/Riverpod进行状态管理
    • 图片缓存优化
    • 网络请求优化
  3. 国际化

    • 支持多语言切换
    • 使用intl库进行日期格式化
  4. UI美化

    • 添加更多动画效果
    • 支持深色模式
    • 自定义主题

6.4 学习收获

通过这个实践案例,我们学习了:

  • Flutter在鸿蒙平台上的完整开发流程
  • 如何集成和使用多个三方库
  • MVVM架构模式的实际应用
  • 数据持久化和缓存策略
  • 异步编程和错误处理
  • 跨平台开发的最佳实践

七、常见问题

7.1 鸿蒙平台相关

Q: Flutter应用无法在鸿蒙设备上运行?
A: 确保Flutter版本支持鸿蒙,并正确配置了鸿蒙开发环境。检查系统版本是否为HarmonyOS 6.0+。

Q: 定位权限无法获取?
A: 检查module.json5中的权限配置,确保使用APPROXIMATELY_LOCATION权限,并确认用户已授权。

Q: API版本不兼容怎么办?
A: 确保DevEco Studio安装了API 20+ SDK,在pubspec.yaml中配置正确的compileSdkVersion和targetSdkVersion。

Q: 模糊定位不工作?
A: API 20+支持模糊定位,确保使用LocationAccuracy.medium或更低精度,并在module.json5中配置正确的权限。

7.2 三方库相关

Q: http库请求失败?
A: 检查网络连接,确认API密钥正确,查看鸿蒙网络权限配置。

Q: shared_preferences数据丢失?
A: 鸿蒙系统清理缓存时会清除shared_preferences数据,这是正常行为。

7.3 开发调试

Q: 如何查看鸿蒙日志?
A: 使用DevEco Studio的日志工具或命令行工具查看。

Q: 热重载在鸿蒙上不工作?
A: 鸿蒙平台对热重载的支持有限,建议使用完全重启。


结语

本案例通过一个完整的天气应用,展示了Flutter在鸿蒙平台上的开发流程和三方库的使用方法。希望这个案例能够帮助新手鸿蒙开发者快速上手Flutter跨端开发,理解跨平台开发的核心理念和实践技巧。

Flutter的生态非常丰富,三方库众多,合理选择和使用这些库可以大大提高开发效率。在实际项目中,建议根据具体需求选择合适的库,并遵循最佳实践进行开发。

祝大家在Flutter鸿蒙开发的道路上越走越远!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐