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


目录


一、前言

作为一名鸿蒙开发者,你可能已经掌握了ArkTS和鸿蒙原生应用开发。但随着Flutter对鸿蒙平台的正式支持,现在你可以使用Flutter编写一次代码,同时运行在鸿蒙、Android、iOS等多个平台上。

本教程将带你从零开始,使用Flutter在鸿蒙平台上构建一个真实的天气应用。我们将重点学习:

  • ✅ Flutter鸿蒙开发环境搭建
  • 三方库的集成与使用(httpproviderintl
  • ✅ 网络请求与JSON数据解析
  • ✅ 响应式状态管理(Provider模式)
  • ✅ 国际化日期格式化
  • ✅ 在鸿蒙设备上运行Flutter应用
  • ✅ DevEco Studio调试与运行

项目案例:构建一个"城市天气查询"应用,通过调用OpenWeatherMap免费天气API获取实时天气数据,并以美观的UI展示在屏幕上。


二、开发环境准备

2.1 安装必备工具

工具 版本要求 说明
Flutter SDK 3.16+(支持鸿蒙) 支持鸿蒙的Flutter版本
DevEco Studio 4.0+ 鸿蒙开发IDE
Dart SDK 3.2+ 随Flutter一起安装
Node.js 16.x - 20.x 鸿蒙构建依赖(版本很重要!)
ohpm 最新 鸿蒙包管理器

2.2 验证Flutter环境

打开终端(命令行),运行以下命令检查Flutter安装情况:

# 检查Flutter版本
flutter --version

# 检查鸿蒙平台是否已配置
flutter config --list

# 查看可用设备
flutter devices

2.3 配置鸿蒙支持

# 启用鸿蒙平台支持
flutter config --enable-ohos

# 检查Flutter doctor输出
flutter doctor

注意:确保 flutter doctor 显示鸿蒙相关配置为绿色勾选状态。如有红色叉号,请根据提示安装缺失的组件。


三、创建Flutter项目

3.1 创建新项目

在终端中执行以下命令,创建Flutter项目:

# 创建Flutter项目(包含鸿蒙平台支持)
flutter create --platforms=ohos flutter_harmonyos

# 进入项目目录
cd flutter_harmonyos

3.2 项目结构说明

项目创建成功后,你会看到以下关键目录:

flutter_harmonyos/
├── lib/                    # Dart代码目录(我们主要工作的地方)
│   └── main.dart           # 应用入口文件
├── ohos/                   # 鸿蒙平台相关代码(自动生成)
│   ├── entry/              # 鸿蒙应用入口模块
│   │   ├── src/main/
│   │   │   ├── module.json5  # 鸿蒙模块配置(权限在这里配置)
│   │   │   └── ets/          # 鸿蒙原生代码
│   │   ├── build-profile.json5
│   │   └── oh-package.json5
│   ├── AppScope/           # 应用全局配置
│   └── build-profile.json5
├── pubspec.yaml            # 项目依赖配置文件(类似package.json)
├── test/                   # 测试代码
└── ...

关键点:作为Flutter开发者,我们主要关注 lib/ 目录下的Dart代码和 pubspec.yaml 配置文件。鸿蒙平台的构建配置由Flutter工具自动管理。

3.3 运行初始项目

# 连接鸿蒙设备或启动模拟器后运行
flutter run -d ohos

四、集成三方库

4.1 什么是三方库?

三方库(第三方库)是由社区或第三方开发者提供的代码包,可以帮助我们快速实现常见功能,而无需从零开始编写。Flutter拥有丰富的包生态系统(https://pub.dev)。

4.2 本案例使用的三方库

库名 版本 用途
http ^1.1.0 发起HTTP网络请求,获取天气数据
provider ^6.1.1 状态管理,实现数据与UI的响应式绑定
intl ^0.18.1 国际化支持,用于日期格式化

4.3 配置pubspec.yaml

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 部分添加三方库:

name: flutter_harmonyos
description: "Flutter鸿蒙天气应用实践项目"
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ^3.6.2

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  
  # 三方库依赖
  http: ^1.1.0          # HTTP请求库,用于调用天气API
  provider: ^6.1.1      # 状态管理库,用于管理应用状态
  intl: ^0.18.1         # 国际化库,用于日期和时间格式化

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

4.4 安装三方库

在终端执行以下命令安装依赖:

flutter pub get

说明:该命令会下载 pubspec.yaml 中声明的所有三方库到本地缓存,并生成 pubspec.lock 文件锁定版本号,确保团队开发的一致性。


五、项目结构设计

lib/
├── main.dart                    # 应用主入口
├── models/                      
│   └── weather_model.dart       # 天气数据模型(JSON解析)
├── services/
│   ── weather_service.dart     # 网络请求服务(调用API)
├── providers/
│   └── weather_provider.dart    # 状态管理(连接数据与UI)
└── screens/
    └── weather_screen.dart      # 天气展示界面

设计原则:采用分层架构,将数据模型、网络请求、状态管理和UI界面分离,便于维护和扩展。


六、核心代码实现

6.1 数据模型层

创建 lib/models/weather_model.dart 文件:

/// 天气数据模型
/// 该类用于将API返回的JSON数据转换为Dart对象
class WeatherModel {
  // 城市名称
  final String cityName;
  
  // 当前温度(摄氏度)
  final double temperature;
  
  // 天气描述(如"晴天"、"多云"、"小雨")
  final String description;
  
  // 空气湿度(百分比)
  final int humidity;
  
  // 风速(米/秒)
  final double windSpeed;
  
  // 体感温度(摄氏度)
  final double feelsLike;

  /// 构造函数:创建WeatherModel实例时必须提供所有字段
  WeatherModel({
    required this.cityName,
    required this.temperature,
    required this.description,
    required this.humidity,
    required this.windSpeed,
    required this.feelsLike,
  });

  /// 工厂构造函数:从JSON对象创建WeatherModel实例
  /// 
  /// 参数 json: API返回的JSON数据(Map格式)
  /// 
  /// 使用 factory 关键字表示这是一个工厂构造函数,
  /// 它返回一个WeatherModel实例,但不一定每次都创建新实例
  factory WeatherModel.fromJson(Map<String, dynamic> json) {
    return WeatherModel(
      // 从JSON中提取城市名,使用 ?? 提供默认值防止null
      cityName: json['name'] ?? '未知城市',
      
      // 从嵌套的main对象中获取温度数据
      temperature: (json['main']['temp'] as num).toDouble(),
      
      // 获取天气描述:weather是一个数组,取第一个元素的description
      description: json['weather'][0]['description'] ?? '未知',
      
      // 获取湿度数据
      humidity: json['main']['humidity'] as int,
      
      // 获取风速数据
      windSpeed: (json['wind']['speed'] as num).toDouble(),
      
      // 获取体感温度
      feelsLike: (json['main']['feels_like'] as num).toDouble(),
    );
  }

  /// 将对象转换为JSON格式(备用方法,本案例主要用于数据输出)
  Map<String, dynamic> toJson() {
    return {
      'name': cityName,
      'main': {
        'temp': temperature,
        'humidity': humidity,
        'feels_like': feelsLike,
      },
      'weather': [
        {'description': description},
      ],
      'wind': {
        'speed': windSpeed,
      },
    };
  }

  /// 重写toString方法,方便调试时打印对象信息
  
  String toString() {
    return 'WeatherModel(city: $cityName, temp: $temperature°C, '
        'desc: $description, humidity: $humidity%)';
  }
}

关键知识点

  • factory 构造函数:用于从JSON创建对象,是实现数据解析的核心
  • ?? 空值合并操作符:提供默认值防止null
  • as num 类型转换:确保数值类型正确

6.2 网络请求服务层

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

import 'dart:convert';  // JSON编码解码库
import 'package:http/http.dart' as http;  // 引入http三方库
import '../models/weather_model.dart';    // 导入天气数据模型

/// 天气服务类
/// 负责与天气API进行通信,获取天气数据
class WeatherService {
  // OpenWeatherMap API的基础URL
  static const String _baseUrl = 'https://api.openweathermap.org/data/2.5';
  
  // 你的API Key(需要到 https://openweathermap.org/api 免费注册获取)
  static const String _apiKey = '你的API_KEY_HERE';

  /// 根据城市名称获取天气信息
  /// 
  /// 参数 cityName: 要查询的城市名称(如 "Beijing"、"Shanghai")
  /// 返回: 包含天气数据的WeatherModel对象
  static Future<WeatherModel> getWeatherByCity(String cityName) async {
    // 构建完整的API请求URL
    // units=metric 表示使用摄氏度
    // lang=zh_cn 表示返回中文的天气描述
    final url = Uri.parse(
      '$_baseUrl/weather?q=$cityName&appid=$_apiKey&units=metric&lang=zh_cn',
    );

    try {
      // 发起HTTP GET请求
      final response = await http.get(url);

      // 检查HTTP响应状态码
      if (response.statusCode == 200) {
        // 将响应体(JSON字符串)解码为Map对象
        final Map<String, dynamic> jsonData = json.decode(response.body);
        
        // 使用工厂构造函数将JSON数据转换为WeatherModel对象
        return WeatherModel.fromJson(jsonData);
      } else if (response.statusCode == 404) {
        throw Exception('未找到城市:$cityName,请检查城市名称是否正确');
      } else {
        throw Exception('请求失败,状态码:${response.statusCode}');
      }
    } catch (e) {
      throw Exception('获取天气数据失败:${e.toString()}');
    }
  }

  /// 批量获取多个城市的天气信息
  static Future<List<WeatherModel>> getWeatherForMultipleCities(
    List<String> cityNames,
  ) async {
    // 使用Future.wait并发请求多个城市,提高效率
    final futures = cityNames.map((city) => getWeatherByCity(city));
    return Future.wait(futures);
  }
}

关键知识点

  • http.get():发起HTTP GET请求
  • async/await:Dart的异步编程模式
  • json.decode():将JSON字符串转换为Dart对象
  • Future.wait():并发执行多个异步操作

6.3 状态管理层(Provider模式)

创建 lib/providers/weather_provider.dart 文件:

import 'package:flutter/foundation.dart';  // Flutter基础库,提供ChangeNotifier
import '../models/weather_model.dart';     // 导入天气模型
import '../services/weather_service.dart'; // 导入天气服务

/// 天气状态管理类
/// 
/// 继承自 ChangeNotifier,这是 provider 三方库提供的状态管理基类
/// 当数据发生变化时,调用 notifyListeners() 会通知所有监听的Widget重新构建
class WeatherProvider extends ChangeNotifier {
  // 当前天气数据(私有变量)
  WeatherModel? _currentWeather;
  
  // 加载状态:true表示正在请求数据,UI应该显示加载动画
  bool _isLoading = false;
  
  // 错误信息(私有变量)
  String? _errorMessage;

  // Getter方法:提供公共接口访问私有变量
  WeatherModel? get currentWeather => _currentWeather;
  bool get isLoading => _isLoading;
  String? get errorMessage => _errorMessage;
  bool get hasData => _currentWeather != null;

  /// 获取指定城市的天气数据
  Future<void> fetchWeather(String cityName) async {
    _isLoading = true;
    _errorMessage = null;
    notifyListeners();  // 通知UI更新

    try {
      _currentWeather = await WeatherService.getWeatherByCity(cityName);
      _errorMessage = null;
    } catch (e) {
      _errorMessage = e.toString();
      _currentWeather = null;
    } finally {
      _isLoading = false;
      notifyListeners();  // 再次通知UI更新
    }
  }

  /// 清除当前数据并重置状态
  void clearData() {
    _currentWeather = null;
    _errorMessage = null;
    _isLoading = false;
    notifyListeners();
  }
}

关键知识点

  • ChangeNotifier:Provider库的核心类,实现观察者模式
  • notifyListeners():通知所有订阅者数据已变化
  • try-catch-finally:完整的异常处理流程

6.4 UI界面层

创建 lib/screens/weather_screen.dart 文件:

重要提示:由于需要使用 TextEditingController 并在组件销毁时释放资源,这里使用 StatefulWidget 而不是 StatelessWidget。StatefulWidget 提供了 dispose() 生命周期方法。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/weather_provider.dart';

/// 天气展示界面
/// 
/// 使用StatefulWidget,因为需要管理TextEditingController的生命周期
class WeatherScreen extends StatefulWidget {
  const WeatherScreen({super.key});

  
  State<WeatherScreen> createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State<WeatherScreen> {
  final TextEditingController _cityController = TextEditingController();

  
  void dispose() {
    // 释放控制器资源,防止内存泄漏
    _cityController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    // Consumer监听WeatherProvider的变化
    return Consumer<WeatherProvider>(
      builder: (context, weatherProvider, child) {
        if (weatherProvider.isLoading) {
          return _buildLoadingView();
        }

        if (weatherProvider.errorMessage != null) {
          return _buildErrorView(weatherProvider.errorMessage!);
        }

        if (weatherProvider.hasData) {
          return _buildWeatherView(weatherProvider);
        }

        return _buildSearchView(context, weatherProvider);
      },
    );
  }

  /// 搜索界面(初始状态)
  Widget _buildSearchView(BuildContext context, WeatherProvider provider) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('天气查询'),
        centerTitle: true,
        backgroundColor: Colors.blue,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.wb_sunny, size: 100, color: Colors.orange),
            const SizedBox(height: 30),
            const Text('输入城市名称,查询天气信息',
                style: TextStyle(fontSize: 18, color: Colors.grey)),
            const SizedBox(height: 30),
            TextField(
              controller: _cityController,
              decoration: InputDecoration(
                hintText: '例如:Beijing',
                prefixIcon: const Icon(Icons.location_city),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                filled: true,
                fillColor: Colors.grey[100],
              ),
              onSubmitted: (value) {
                if (value.isNotEmpty) {
                  provider.fetchWeather(value);
                }
              },
            ),
            const SizedBox(height: 16),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  final city = _cityController.text.trim();
                  if (city.isNotEmpty) {
                    provider.fetchWeather(city);
                  } else {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('请输入城市名称'),
                        duration: Duration(seconds: 2),
                      ),
                    );
                  }
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.blue,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: const Text('查询天气',
                    style: TextStyle(fontSize: 18, color: Colors.white)),
              ),
            ),
            const SizedBox(height: 20),
            const Text('热门城市:', style: TextStyle(fontSize: 16)),
            const SizedBox(height: 10),
            Wrap(
              spacing: 10,
              runSpacing: 10,
              children: [
                _buildCityChip('Beijing', provider),
                _buildCityChip('Shanghai', provider),
                _buildCityChip('Guangzhou', provider),
                _buildCityChip('Shenzhen', provider),
                _buildCityChip('Tokyo', provider),
              ],
            ),
          ],
        ),
      ),
    );
  }

  /// 构建城市快捷按钮
  Widget _buildCityChip(String cityName, WeatherProvider provider) {
    return ActionChip(
      label: Text(cityName),
      avatar: const Icon(Icons.location_on, size: 18),
      onPressed: () => provider.fetchWeather(cityName),
      backgroundColor: Colors.blue[50],
    );
  }

  /// 天气信息展示界面
  Widget _buildWeatherView(WeatherProvider provider) {
    final weather = provider.currentWeather!;
    final now = DateTime.now();
    final formattedDate = DateFormat('yyyy年MM月dd日 HH:mm').format(now);

    return Scaffold(
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.blue[400]!, Colors.blue[800]!],
          ),
        ),
        child: SafeArea(
          child: Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      icon: const Icon(Icons.arrow_back, color: Colors.white),
                      onPressed: () => provider.clearData(),
                    ),
                    Text(weather.cityName,
                        style: const TextStyle(
                            fontSize: 24,
                            fontWeight: FontWeight.bold,
                            color: Colors.white)),
                    IconButton(
                      icon: const Icon(Icons.refresh, color: Colors.white),
                      onPressed: () => provider.fetchWeather(weather.cityName),
                    ),
                  ],
                ),
              ),
              Expanded(
                child: SingleChildScrollView(
                  child: Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Column(
                      children: [
                        _getWeatherIcon(weather.description),
                        const SizedBox(height: 10),
                        Text('${weather.temperature.round()}°C',
                            style: const TextStyle(
                                fontSize: 72,
                                fontWeight: FontWeight.bold,
                                color: Colors.white)),
                        Text(weather.description,
                            style: const TextStyle(
                                fontSize: 22, color: Colors.white70)),
                        const SizedBox(height: 30),
                        Card(
                          elevation: 8,
                          shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(16)),
                          color: Colors.white.withOpacity(0.2),
                          child: Padding(
                            padding: const EdgeInsets.all(20.0),
                            child: Column(
                              children: [
                                _buildInfoRow(
                                    icon: Icons.thermostat,
                                    label: '体感温度',
                                    value: '${weather.feelsLike.round()}°C'),
                                const Divider(color: Colors.white30),
                                _buildInfoRow(
                                    icon: Icons.water_drop,
                                    label: '湿度',
                                    value: '${weather.humidity}%'),
                                const Divider(color: Colors.white30),
                                _buildInfoRow(
                                    icon: Icons.air,
                                    label: '风速',
                                    value: '${weather.windSpeed} m/s'),
                              ],
                            ),
                          ),
                        ),
                        const SizedBox(height: 20),
                        Text('更新时间:$formattedDate',
                            style: const TextStyle(
                                color: Colors.white60, fontSize: 14)),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  /// 根据天气描述返回对应的图标
  Widget _getWeatherIcon(String description) {
    IconData iconData;
    if (description.contains('晴') || description.contains('clear')) {
      iconData = Icons.wb_sunny;
    } else if (description.contains('云') || description.contains('cloud')) {
      iconData = Icons.cloud;
    } else if (description.contains('雨') || description.contains('rain')) {
      iconData = Icons.beach_access;
    } else if (description.contains('雪') || description.contains('snow')) {
      iconData = Icons.ac_unit;
    } else if (description.contains('雷') || description.contains('thunder')) {
      iconData = Icons.flash_on;
    } else if (description.contains('雾') || description.contains('fog')) {
      iconData = Icons.blur_on;
    } else {
      iconData = Icons.wb_cloudy;
    }
    return Icon(iconData, size: 80, color: Colors.yellow[300]);
  }

  /// 构建信息行
  Widget _buildInfoRow({
    required IconData icon,
    required String label,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Row(children: [
            Icon(icon, color: Colors.white),
            const SizedBox(width: 10),
            Text(label, style: const TextStyle(fontSize: 16, color: Colors.white)),
          ]),
          Text(value,
              style: const TextStyle(
                  fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
        ],
      ),
    );
  }

  /// 加载中视图
  Widget _buildLoadingView() {
    return const Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Colors.blue)),
            SizedBox(height: 20),
            Text('正在获取天气数据...',
                style: TextStyle(fontSize: 16, color: Colors.grey)),
          ],
        ),
      ),
    );
  }

  /// 错误视图
  Widget _buildErrorView(String errorMessage) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('天气查询'),
        backgroundColor: Colors.red,
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.error_outline, size: 80, color: Colors.red),
              const SizedBox(height: 20),
              const Text('获取数据失败',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              const SizedBox(height: 10),
              Text(errorMessage, textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 14, color: Colors.grey)),
              const SizedBox(height: 30),
              ElevatedButton.icon(
                onPressed: () {
                  Provider.of<WeatherProvider>(context, listen: false).clearData();
                  _cityController.clear();
                },
                icon: const Icon(Icons.refresh),
                label: const Text('返回重试'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.blue,
                  padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

关键知识点

  • StatefulWidget vs StatelessWidget:当需要管理控制器生命周期(如 TextEditingController)时,必须使用 StatefulWidget,它提供 dispose() 方法
  • Consumer<WeatherProvider>:Provider的核心组件,监听状态变化
  • context 在 StatefulWidget 的 State 中可以直接访问
  • SingleChildScrollView:防止内容溢出屏幕

6.5 主入口文件

更新 lib/main.dart 文件:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'providers/weather_provider.dart';
import 'screens/weather_screen.dart';

void main() async {
  // 确保Flutter框架已完全初始化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化intl本地化数据(必须在使用DateFormat前调用)
  await initializeDateFormatting('zh_CN', null);
  
  // 运行应用
  runApp(const WeatherApp());
}

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

  
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => WeatherProvider()),
      ],
      child: MaterialApp(
        title: '天气查询',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: WeatherScreen(),
      ),
    );
  }
}

关键知识点

  • initializeDateFormatting('zh_CN', null)必须在使用 DateFormat 前调用,否则日期格式化会失败
  • MultiProvider:允许同时提供多个Provider
  • ChangeNotifierProvider:创建并注入Provider到Widget树

七、鸿蒙端配置与运行

7.1 获取天气API Key

  1. 访问 https://openweathermap.org/api
  2. 注册免费账号
  3. 在 API Keys 页面生成你的 Key
  4. 将 Key 替换到 lib/services/weather_service.dart 中的 _apiKey 变量
static const String _apiKey = '你的API_KEY';  // 替换这里

7.2 配置网络权限

打开 ohos/entry/src/main/module.json5,确保包含网络权限:

{
  "module": {
    "requestPermissions": [
      {"name": "ohos.permission.INTERNET"}
    ]
  }
}

7.3 检查Node.js版本

确保 Node.js 版本在 16.x - 20.x 范围内:

node -v

如果版本不对,在 DevEco Studio 中:

  • 打开 File → Settings → Languages & Frameworks → Node.js
  • 设置正确的 Node.js 路径

7.4 运行项目

方式一:使用DevEco Studio运行(推荐)
  1. 用 DevEco Studio 打开 ohos/ 目录
  2. 启动鸿蒙模拟器或连接真机
  3. 选择目标设备
  4. 点击 Run 按钮运行
方式二:使用命令行运行
# 查看可用设备
flutter devices

# 运行到鸿蒙设备
flutter run -d ohos

7.5 构建发布版本

# 构建鸿蒙Release版本
flutter build ohos --release

# 构建产物位置
# ohos/build/outputs/default/entry-default-signed.hap

八、代码检查与构建

8.1 运行代码分析

在编写完代码后,运行静态分析检查代码质量:

flutter analyze

该命令会检查:

  • 语法错误
  • 类型错误
  • 潜在的空指针问题
  • 未使用的变量
  • 代码规范问题

8.2 构建HAP包

# Debug构建
flutter build ohos --debug

# Release构建
flutter build ohos --release

构建产物位置:ohos/entry/build/default/outputs/


九、常见问题与解决方案

9.1 编译错误:context isn’t defined

问题

Error: The getter 'context' isn't defined for the class 'WeatherScreen'.

原因:使用了 StatelessWidget,但 context 在某些回调中不可访问。

解决方案:将 StatelessWidget 改为 StatefulWidget

// 错误写法
class WeatherScreen extends StatelessWidget {
  final TextEditingController _cityController = TextEditingController();
  
  
  void dispose() {  // 错误:StatelessWidget没有dispose方法
    _cityController.dispose();
  }
}

// 正确写法
class WeatherScreen extends StatefulWidget {
  
  State<WeatherScreen> createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State<WeatherScreen> {
  final TextEditingController _cityController = TextEditingController();
  
  
  void dispose() {
    _cityController.dispose();
    super.dispose();
  }
}

9.2 编译错误:Superclass has no method named ‘dispose’

问题

Error: Superclass has no method named 'dispose'.

原因StatelessWidget 没有 dispose() 生命周期方法。

解决方案:同上,改用 StatefulWidget

9.3 intl日期格式化不生效

问题DateFormat 报错或显示异常。

解决方案:在 main() 中初始化本地化:

import 'package:intl/date_symbol_data_local.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDateFormatting('zh_CN', null);  // 必须调用
  runApp(const WeatherApp());
}

9.4 Provider报错

问题Could not find the correct Provider<WeatherProvider>

解决方案:确保 ChangeNotifierProviderMaterialApp 的外层:

return MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => WeatherProvider()),
  ],
  child: MaterialApp(...),  // MaterialApp必须在Provider内部
);

9.5 三方库依赖冲突

问题flutter pub get 报版本冲突。

解决方案

# 清理缓存并重新获取依赖
flutter clean
flutter pub get

9.6 网络请求失败

问题:显示"获取天气数据失败"

解决方案

  1. 检查设备是否联网
  2. 确认 module.json5 中已添加 ohos.permission.INTERNET 权限
  3. 验证 API Key 是否正确
  4. 检查城市名称是否为英文(如 “Beijing” 而非 “北京”)

9.7 设备连接问题

问题[E001005] Device not found or connected

解决方案

  1. 重启模拟器(Device Manager → Cold Boot)
  2. 在 DevEco Studio 终端执行 hdc kill -r 然后 hdc start
  3. 如果模拟器持续闪退,尝试增加模拟器内存或创建新模拟器
  4. 使用真机调试更稳定

9.8 工程同步失败

问题:DevEco Studio 显示"工程同步失败,一些基础功能可能失效"

解决方案

  1. 点击提示条上的 Try Again 重试
  2. File → Invalidate Caches… → 勾选所有选项 → Invalidate and Restart
  3. 检查 Node.js 版本是否在 16.x - 20.x 范围内
  4. 在 DevEco Studio 终端执行:
    cd ohos
    hvigorw --stop-daemon
    hvigorw clean
    
  5. 确认 HarmonyOS SDK 路径和版本配置正确

9.9 鸿蒙设备不显示

问题flutter devices 没有显示鸿蒙设备

解决方案

  1. 确保已启用鸿蒙支持:flutter config --enable-ohos
  2. 检查模拟器/真机连接状态
  3. 确认 DevEco Studio 能正常识别设备
  4. 重启 DevEco Studio

十、总结

恭喜!你已经完成了一个完整的 Flutter 鸿蒙天气应用开发实践。让我们回顾一下本教程的核心要点:

🎯 知识回顾

知识领域 核心内容
Flutter基础 Widget树、StatefulWidget、MaterialApp、Scaffold
三方库使用 http(网络请求)、provider(状态管理)、intl(日期格式化)
架构设计 分层架构:Model-Service-Provider-View
网络编程 HTTP请求、JSON解析、异步编程(async/await)
状态管理 ChangeNotifier、Consumer、notifyListeners
UI设计 渐变背景、卡片布局、响应式设计
鸿蒙集成 权限配置、设备运行、HAP构建

📚 进阶学习方向

  1. 更多三方库shared_preferences(本地存储)、cached_network_image(图片缓存)、dio(高级HTTP客户端)
  2. 高级状态管理:学习 RiverpodGetX 等更强大的方案
  3. 路由管理:使用 go_router 实现多页面导航
  4. 动画效果:学习Flutter动画系统,添加流畅过渡效果
  5. 单元测试:为业务逻辑编写单元测试
  6. 鸿蒙特性:探索万能卡片、分布式能力等

💡 最佳实践建议

  • ✅ 使用分层架构,保持代码可维护性
  • ✅ 合理使用三方库,避免重复造轮子
  • ✅ 做好异常处理,提升用户体验
  • ✅ 及时释放资源(dispose),防止内存泄漏
  • ✅ 使用 const 构造函数优化性能
  • ✅ 在 main() 中正确初始化 intl 本地化
  • ✅ 使用 StatefulWidget 管理需要生命周期的资源

🔗 相关资源

  • Flutter官方文档:https://flutter.dev/docs
  • Flutter三方库市场:https://pub.dev
  • 鸿蒙开发文档:https://developer.harmonyos.com
  • OpenWeatherMap API:https://openweathermap.org/api

最后的话:Flutter for HarmonyOS 是一个正在快速发展的领域,本教程为你打下了坚实的基础。继续实践、继续学习,你将成为一名优秀的跨平台开发者!

祝开发愉快! 🚀

模拟运行成功图片:
在这里插入图片描述
在这里插入图片描述

Logo

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

更多推荐