【maaath】Flutter for OpenHarmony:跨平台天气应用开发指南

Flutter for OpenHarmony 实战:跨平台天气应用开发指南

作者:maaath


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


前言

在移动应用开发领域,跨平台技术一直是开发者关注的焦点。Flutter 作为 Google 推出的跨平台 UI 框架,凭借其高性能和丰富的生态体系,被广泛应用于 iOS、Android 以及嵌入式设备开发。而 OpenHarmony 作为国产操作系统的代表,正在积极拥抱跨平台开发能力。本文将通过一个实际的天气应用案例,详细介绍如何在 OpenHarmony 设备上运行 Flutter 应用,并实现原生能力与跨平台代码的协同工作。

本文将手把手教你如何将一个完整的 Flutter 天气应用集成到 OpenHarmony 项目中,包括网络请求、城市搜索、本地存储、下拉刷新以及动画效果等核心功能的实现。通过这个实战项目,你将掌握 Flutter 与 OpenHarmony 混合开发的核心技巧。

一、项目概述

1.1 项目背景

本项目是一个集成了 Flutter 模块的 OpenHarmony 应用示例,主要展示了以下技术能力:

  • Flutter 模块集成:在 OpenHarmony 原生应用中嵌入 Flutter 页面
  • 跨平台 UI 开发:使用 Flutter 实现天气展示界面
  • 状态管理:使用 Provider 进行状态管理
  • 网络请求:通过 HTTP 请求获取天气数据
  • 本地存储:使用 shared_preferences 持久化用户数据
  • 动画效果:实现天气图标动画和页面过渡动画

1.2 技术架构

项目采用分层架构设计,将业务逻辑、数据层和 UI 层分离:

复制代码
┌─────────────────────────────────────┐
│          OpenHarmony 原生层          │
│  (EntryAbility、Index页面、路由)     │
├─────────────────────────────────────┤
│          Flutter 嵌入层              │
│      (FlutterPage 组件)             │
├─────────────────────────────────────┤
│          Flutter 业务层              │
│  (Provider、Service、Models)        │
├─────────────────────────────────────┤
│          Flutter UI 层              │
│   (页面、组件、动画)                 │
└─────────────────────────────────────┘

1.3 项目结构

复制代码
ohos/
├── entry/
│   └── src/main/ets/
│       ├── entryability/            # 原生 Ability
│       └── pages/                   # 原生页面
│           └── Index.ets            # 集成 Flutter 的主页
├── flutter_weather/                 # Flutter 模块
│   └── lib/
│       ├── main.dart               # Flutter 入口
│       ├── models/                  # 数据模型
│       ├── pages/                   # 页面
│       ├── providers/               # 状态管理
│       ├── services/                # 服务层
│       └── widgets/                 # 组件
└── lib/                            # Flutter 业务代码(编译输出)

二、核心功能实现

2.1 天气数据模型

首先定义天气数据的数据模型,包括城市信息、天气状况、温度等核心字段:

dart 复制代码
class WeatherData {
  final String cityName;           // 城市名称
  final double temperature;         // 温度(摄氏度)
  final double feelsLike;          // 体感温度
  final int humidity;              // 湿度
  final double windSpeed;          // 风速
  final String description;        // 天气描述
  final String icon;               // 天气图标代码
  final DateTime timestamp;         // 数据时间戳

  WeatherData({
    required this.cityName,
    required this.temperature,
    required this.feelsLike,
    required this.humidity,
    required this.windSpeed,
    required this.description,
    required this.icon,
    required this.timestamp,
  });

  factory WeatherData.fromJson(Map<String, dynamic> json) {
    return WeatherData(
      cityName: json['name'] ?? '',
      temperature: (json['main']['temp'] as num).toDouble(),
      feelsLike: (json['main']['feels_like'] as num).toDouble(),
      humidity: json['main']['humidity'] as int,
      windSpeed: (json['wind']['speed'] as num).toDouble(),
      description: json['weather'][0]['description'] ?? '',
      icon: json['weather'][0]['icon'] ?? '01d',
      timestamp: DateTime.now(),
    );
  }
}

class CityInfo {
  final String name;               // 城市名称
  final String country;            // 国家
  final double lat;                // 纬度
  final double lon;                // 经度

  CityInfo({
    required this.name,
    required this.country,
    required this.lat,
    required this.lon,
  });

  factory CityInfo.fromJson(Map<String, dynamic> json) {
    return CityInfo(
      name: json['name'] ?? '',
      country: json['country'] ?? '',
      lat: (json['lat'] as num).toDouble(),
      lon: (json['lon'] as num).toDouble(),
    );
  }
}

2.2 网络请求服务

天气数据通过 OpenWeatherMap API 获取,以下是网络请求服务的实现:

dart 复制代码
class WeatherService {
  // API 配置
  static const String _apiKey = 'YOUR_API_KEY_HERE';
  static const String _baseUrl = 'https://api.openweathermap.org/data/2.5';

  final http.Client _client;

  WeatherService({http.Client? client}) : _client = client ?? http.Client();

  // 获取城市天气
  Future<WeatherData> getWeatherByCity(String city) async {
    final url = '$_baseUrl/weather?q=$city&appid=$_apiKey&units=metric';
    final response = await _client.get(Uri.parse(url));

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      return WeatherData.fromJson(json);
    } else {
      throw WeatherException('Failed to load weather: ${response.statusCode}');
    }
  }

  // 搜索城市
  Future<List<CityInfo>> searchCities(String query) async {
    final url = '$_baseUrl/find?q=$query&appid=$_apiKey';
    final response = await _client.get(Uri.parse(url));

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body) as Map<String, dynamic>;
      final list = json['list'] as List<dynamic>;
      return list.map((item) => CityInfo.fromJson(item)).toList();
    } else {
      return [];
    }
  }

  void dispose() {
    _client.close();
  }
}

class WeatherException implements Exception {
  final String message;
  WeatherException(this.message);

  @override
  String toString() => message;
}

2.3 本地存储服务

使用 shared_preferences 实现用户偏好设置的持久化存储:

dart 复制代码
class StorageService {
  static late SharedPreferences _prefs;

  static Future<StorageService> init() async {
    _prefs = await SharedPreferences.getInstance();
    return StorageService();
  }

  // 城市管理
  Future<void> saveCurrentCity(CityInfo city) async {
    await _prefs.setString('current_city', city.name);
    await _prefs.setString('current_country', city.country);
  }

  CityInfo? getCurrentCity() {
    final name = _prefs.getString('current_city');
    if (name == null) return null;
    return CityInfo(
      name: name,
      country: _prefs.getString('current_country') ?? '',
      lat: 0,
      lon: 0,
    );
  }

  // 收藏城市
  Future<void> addFavoriteCity(CityInfo city) async {
    final favorites = getFavoriteCities();
    if (!favorites.any((c) => c.name == city.name)) {
      favorites.add(city);
      await _saveFavorites(favorites);
    }
  }

  Future<void> removeFavoriteCity(CityInfo city) async {
    final favorites = getFavoriteCities();
    favorites.removeWhere((c) => c.name == city.name);
    await _saveFavorites(favorites);
  }

  List<CityInfo> getFavoriteCities() {
    final data = _prefs.getStringList('favorite_cities') ?? [];
    return data.map((s) {
      final parts = s.split(',');
      return CityInfo(name: parts[0], country: parts[1], lat: 0, lon: 0);
    }).toList();
  }

  Future<void> _saveFavorites(List<CityInfo> favorites) async {
    final data = favorites.map((c) => '${c.name},${c.country}').toList();
    await _prefs.setStringList('favorite_cities', data);
  }

  // 设置管理
  bool getTemperatureUnit() => _prefs.getBool('use_celsius') ?? true;
  Future<void> setTemperatureUnit(bool value) async {
    await _prefs.setBool('use_celsius', value);
  }

  bool getThemeMode() => _prefs.getBool('dark_mode') ?? false;
  Future<void> setThemeMode(bool value) async {
    await _prefs.setBool('dark_mode', value);
  }
}

2.4 状态管理

使用 Provider 进行全局状态管理,实现天气数据、收藏城市和应用设置的统一管理:

dart 复制代码
class WeatherProvider extends ChangeNotifier {
  final WeatherService _weatherService;
  late StorageService _storage;

  WeatherData? _currentWeather;
  List<CityInfo> _favoriteCities = [];
  bool _isLoading = false;
  String? _error;
  bool _useCelsius = true;
  bool _isDarkMode = false;

  WeatherProvider({WeatherService? weatherService})
      : _weatherService = weatherService ?? WeatherService();

  // Getter 方法
  WeatherData? get currentWeather => _currentWeather;
  List<CityInfo> get favoriteCities => _favoriteCities;
  bool get isLoading => _isLoading;
  String? get error => _error;
  bool get useCelsius => _useCelsius;
  bool get isDarkMode => _isDarkMode;

  // 初始化
  Future<void> initialize(StorageService storage) async {
    _storage = storage;
    _favoriteCities = storage.getFavoriteCities();
    _useCelsius = storage.getTemperatureUnit();
    _isDarkMode = storage.getThemeMode();

    final currentCity = storage.getCurrentCity();
    await loadWeatherByCity(currentCity?.name ?? 'Beijing');
    notifyListeners();
  }

  // 加载天气数据
  Future<void> loadWeatherByCity(String city) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      _currentWeather = await _weatherService.getWeatherByCity(city);
      _error = null;
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  // 刷新天气
  Future<void> refreshWeather() async {
    if (_currentWeather != null) {
      await loadWeatherByCity(_currentWeather!.cityName);
    }
  }

  // 温度格式化
  String formatTemperature(double celsius) {
    if (_useCelsius) {
      return '${celsius.round()}°C';
    } else {
      final fahrenheit = celsius * 9 / 5 + 32;
      return '${fahrenheit.round()}°F';
    }
  }

  // 收藏管理
  Future<void> addFavorite(CityInfo city) async {
    await _storage.addFavoriteCity(city);
    _favoriteCities = _storage.getFavoriteCities();
    notifyListeners();
  }

  Future<void> removeFavorite(CityInfo city) async {
    await _storage.removeFavoriteCity(city);
    _favoriteCities = _storage.getFavoriteCities();
    notifyListeners();
  }
}

2.5 天气图标动画

使用 CustomPainter 实现丰富的天气动画效果,包括晴天旋转、降雨、雷电等:

dart 复制代码
class AnimatedWeatherIcon extends StatefulWidget {
  final String condition;
  final double size;

  const AnimatedWeatherIcon({
    super.key,
    required this.condition,
    this.size = 100,
  });

  @override
  State<AnimatedWeatherIcon> createState() => _AnimatedWeatherIconState();
}

class _AnimatedWeatherIconState extends State<AnimatedWeatherIcon>
    with TickerProviderStateMixin {
  late AnimationController _primaryController;
  late AnimationController _secondaryController;

  @override
  void initState() {
    super.initState();
    _primaryController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();
    _secondaryController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
  }

  @override
  void dispose() {
    _primaryController.dispose();
    _secondaryController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _primaryController,
      builder: (context, child) {
        return CustomPaint(
          size: Size(widget.size, widget.size),
          painter: _getPainter(),
        );
      },
    );
  }

  WeatherPainter _getPainter() {
    if (widget.condition.contains('rain')) {
      return RainPainter(_primaryController.value);
    } else if (widget.condition.contains('thunder')) {
      return ThunderPainter(_primaryController.value);
    } else if (widget.condition.contains('snow')) {
      return SnowPainter(_primaryController.value);
    } else if (widget.condition.contains('cloud')) {
      return CloudPainter(_primaryController.value);
    } else if (widget.condition.contains('fog') ||
               widget.condition.contains('mist')) {
      return FogPainter(_primaryController.value);
    }
    return SunPainter(_primaryController.value);
  }
}

class SunPainter extends CustomPainter {
  final double animation;

  SunPainter(this.animation);

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width * 0.25;

    // 太阳主体
    final sunPaint = Paint()
      ..color = const Color(0xFFFFC107)
      ..style = PaintingStyle.fill;
    canvas.drawCircle(center, radius, sunPaint);

    // 旋转光芒
    final rayPaint = Paint()
      ..color = const Color(0xFFFFC107)
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round;

    for (int i = 0; i < 8; i++) {
      final angle = (i * 45 + animation * 360) * pi / 180;
      final start = center + Offset(cos(angle), sin(angle)) * radius * 1.3;
      final end = center + Offset(cos(angle), sin(angle)) * radius * 1.8;
      canvas.drawLine(start, end, rayPaint);
    }
  }

  @override
  bool shouldRepaint(covariant SunPainter oldDelegate) {
    return oldDelegate.animation != animation;
  }
}

class RainPainter extends CustomPainter {
  final double animation;

  RainPainter(this.animation);

  @override
  void paint(Canvas canvas, Size size) {
    final cloudCenter = Offset(size.width / 2, size.height * 0.3);
    final cloudPaint = Paint()
      ..color = Colors.grey.shade400
      ..style = PaintingStyle.fill;
    canvas.drawCircle(cloudCenter, size.width * 0.2, cloudPaint);

    final dropPaint = Paint()
      ..color = const Color(0xFF2196F3)
      ..strokeWidth = 2
      ..strokeCap = StrokeCap.round;

    for (int i = 0; i < 6; i++) {
      final x = size.width * 0.25 + i * size.width * 0.1;
      final dropAnimation = (animation + i * 0.15) % 1.0;
      final y = size.height * 0.4 + dropAnimation * size.height * 0.5;
      canvas.drawLine(
        Offset(x, y),
        Offset(x - 5, y + 15),
        dropPaint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant RainPainter oldDelegate) {
    return oldDelegate.animation != animation;
  }
}

2.6 天气主页面

整合所有组件,实现下拉刷新、天气展示和底部导航:

dart 复制代码
class WeatherPage extends StatelessWidget {
  const WeatherPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<WeatherProvider>(
      builder: (context, provider, child) {
        return RefreshIndicator(
          onRefresh: provider.refreshWeather,
          child: CustomScrollView(
            physics: const AlwaysScrollableScrollPhysics(),
            slivers: [
              // 天气主显示区
              SliverToBoxAdapter(
                child: _buildMainWeather(provider),
              ),
              // 天气详情卡片
              SliverToBoxAdapter(
                child: _buildWeatherDetails(provider),
              ),
              // 底部占位
              const SliverToBoxAdapter(
                child: SizedBox(height: 100),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildMainWeather(WeatherProvider provider) {
    if (provider.isLoading) {
      return const Center(
        child: Padding(
          padding: EdgeInsets.all(50),
          child: CircularProgressIndicator(),
        ),
      );
    }

    final weather = provider.currentWeather;
    if (weather == null) {
      return const Center(
        child: Text('No weather data'),
      );
    }

    return Container(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          Text(
            weather.cityName,
            style: const TextStyle(
              fontSize: 32,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),
          AnimatedWeatherIcon(
            condition: weather.description,
            size: 120,
          ),
          const SizedBox(height: 16),
          Text(
            provider.formatTemperature(weather.temperature),
            style: const TextStyle(
              fontSize: 64,
              fontWeight: FontWeight.w300,
            ),
          ),
          Text(
            weather.description,
            style: const TextStyle(fontSize: 20),
          ),
        ],
      ),
    );
  }

  Widget _buildWeatherDetails(WeatherProvider provider) {
    final weather = provider.currentWeather;
    if (weather == null) return const SizedBox();

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildDetailItem('湿度', '${weather.humidity}%'),
          _buildDetailItem('风速', '${weather.windSpeed} m/s'),
          _buildDetailItem('体感', provider.formatTemperature(weather.feelsLike)),
        ],
      ),
    );
  }

  Widget _buildDetailItem(String label, String value) {
    return Column(
      children: [
        Text(label, style: const TextStyle(color: Colors.white70)),
        const SizedBox(height: 4),
        Text(
          value,
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    );
  }
}

三、项目集成配置

3.1 Flutter 模块配置

在 Flutter 模块的 pubspec.yaml 中添加必要的依赖:

yaml 复制代码
name: flutter_weather
description: Flutter weather app for OpenHarmony
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.0
  http: ^1.1.0
  shared_preferences: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

3.2 OpenHarmony 原生集成

在 OpenHarmony 原生页面中使用 FlutterPage 组件嵌入 Flutter 内容:

typescript 复制代码
import { FlutterPage } from '@ohos/flutter_ohos';

@Entry
@Component
struct Index {
  @LocalStorageLink('viewId') viewId: string = "";

  build() {
    Stack() {
      // Flutter 内容作为背景
      FlutterPage({ viewId: this.viewId })
        .opacity(1)

      // 原生导航浮层
      Column() {
        // 导航内容...
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

四、运行效果

4.1 编译配置

使用 hvigor 进行项目构建:

bash 复制代码
# 清理并重新构建
hvigor clean
hvigor assembleDebug --no-daemon

4.2 运行截图

项目成功运行后的界面截图如下:


界面展示了以下功能模块:

  • 顶部天气卡片:显示城市名称、温度和天气状况
  • 天气图标动画:太阳/云朵/雨滴等动态效果
  • 天气详情:湿度、风速、体感温度等数据展示
  • 底部导航:天气、城市、设置三个 Tab 页面

五、关键技术与最佳实践

5.1 混合开发架构

在进行 Flutter 与 OpenHarmony 混合开发时,需要注意以下要点:

  1. 明确职责边界:原生层负责系统级功能和页面导航,Flutter 层负责业务 UI 和交互
  2. 数据共享:通过 LocalStorage 或平台通道实现原生与 Flutter 的数据通信
  3. 性能优化:避免在 Flutter 中执行重量级计算,合理使用 Isolate

5.2 状态管理建议

对于中等复杂度的应用,推荐使用 Provider 作为状态管理方案:

  • 简单易用:学习曲线平缓,API 设计直观
  • 功能完整:支持依赖注入、响应式更新和作用域控制
  • 性能良好:基于 ChangeNotifier 的发布-订阅模式,开销可控

5.3 网络请求处理

在鸿蒙设备上进行网络请求时,需要注意:

  • 添加网络权限声明
  • 处理网络异常和超时情况
  • 实现请求缓存策略减少流量消耗

5.4 本地存储策略

使用 shared_preferences 存储轻量级数据,如用户偏好设置。对于大量数据,考虑使用 SQLite 或文件存储。

六、总结与展望

本文通过一个完整的天气应用案例,详细介绍了 Flutter 模块集成到 OpenHarmony 项目的全过程,包括数据模型设计、网络请求封装、本地存储实现、状态管理以及动画效果等核心功能。

通过本项目的学习,开发者可以掌握以下技能:

  • Flutter 模块的创建和配置
  • OpenHarmony 原生页面的 Flutter 嵌入
  • 跨平台状态管理方案
  • 网络请求和本地存储的实现
  • 丰富动画效果的制作

未来,随着 Flutter 对 OpenHarmony 支持的不断完善,我们将能够实现更加复杂的跨平台应用,为 OpenHarmony 生态注入更多活力。

代码仓库

本文完整项目代码托管于 AtomGit:

仓库地址https://atomgit.com/maaath/flutter-ohos-weather-demo

仓库包含完整的项目源码、配置文件以及本文涉及的示例代码,开发者可以直接克隆进行学习和实践。


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

感谢各位阅读!

相关推荐
maaath7 小时前
【maaath】Flutter for OpenHarmony 宠物社区应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 实战:健身运动应用的跨平台开发指南
flutter·华为·harmonyos
Swift社区7 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
maaath7 小时前
【maaath】 Flutter for OpenHarmony 新闻资讯应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 跨平台图书阅读应用开发实践
flutter·华为·harmonyos
xmdy58667 小时前
Flutter+开源鸿蒙实战|智联邻里Day2 首页UI开发+全局组件封装+鸿蒙多端适配
flutter·开源·harmonyos
特立独行的猫a8 小时前
移植 vcpkg 到鸿蒙 PC:vcpkg-tool 交叉编译与实践手记
华为·harmonyos·vcpkg·鸿蒙pc·vcpkg-tool
911hzh9 小时前
Flutter 音视频通话集成实战:WebSocket 做信令,WebRTC 传音视频,附详细事件时序图
websocket·flutter·音视频
万添裁9 小时前
huawei 机考
算法·华为·深度优先