目录

  1. 概述
  2. 引入三方库步骤
  3. 天气详情页面实战案例
  4. 常见错误及解决方案
  5. 总结

概述

本章节主要详细介绍在使用跨平台框架Flutter开发鸿蒙应用程序,使用Flutter三方库 flutter_staggered_grid_view 库实现多城市天气卡片堆叠式展示和滑动切换功能。

🎯 本教程目标

通过本教程,你将学会:

  1. ✅ 如何在 Flutter 项目中引入 flutter_staggered_grid_view 相关依赖
  2. ✅ 如何实现多城市天气卡片的堆叠式展示
  3. ✅ 如何实现左右滑动切换城市卡片
  4. 如何在天气详情页面中使用堆叠式卡片实现多城市天气切换
  5. ✅ 如何实现滑动回调和撤销滑动功能
  6. ✅ 最终在鸿蒙设备上运行效果如下图所示

image-20260127160220715

📁 项目文件结构

在开始之前,让我们先了解一下项目结构:

lib/
├── models/                       # 数据模型目录
│   ├── weather_models.dart      # 📊 天气数据模型定义
│   └── weather_models.g.dart     # ⚙️ 自动生成的 JSON 序列化代码
└── screens/                      # 页面文件目录
    └── weather_detail_page.dart  # 🌤️ 天气详情页面(使用堆叠式卡片)

🎯 本教程将修改的文件

严格按照以下顺序修改文件,每个步骤完成后立即验证:

  1. pubspec.yaml - 📦 添加 flutter_staggered_grid_view 依赖
  2. lib/screens/weather_detail_page.dart - 🌤️ 天气详情页面,实现多城市卡片切换

🛠️ 技术栈

🌤️ 功能特性

  • 🎴 堆叠式展示:多个城市天气卡片以堆叠方式展示
  • 👆 左右滑动:支持左右滑动切换城市
  • ↩️ 撤销滑动:支持撤销滑动,回到上一个城市
  • 🔄 滑动回调:提供滑动回调,实时更新当前城市
  • 📱 响应式布局:适配不同屏幕尺寸

引入三方库步骤

📋 流程图概览

📝 开始引入三方库

📄 步骤1.1: 打开 pubspec.yaml 文件

📝 步骤1.2: 添加 flutter_staggered_grid_view 依赖

💾 步骤1.3: 保存文件

⬇️ 步骤2: 运行 flutter pub get

✅ 安装成功?

🔍 步骤3: 验证安装

❌ 检查版本兼容性

🎉 完成引入

📝 步骤 1:添加依赖到 pubspec.yaml

步骤 1.1:打开 pubspec.yaml 文件

文件路径: pubspec.yaml(项目根目录)

操作说明:

  1. 📂 在 IDE 中打开项目根目录
  2. 📄 找到并打开 pubspec.yaml 文件
  3. 👀 确认文件内容,找到 dependencies: 部分
步骤 1.2:添加运行时依赖

位置: pubspec.yaml 文件的 dependencies: 部分(约第49行)

操作步骤:

  1. 📍 找到 dependencies: 部分
  2. 📝 在 json_annotation: ^4.8.1 之后添加以下内容:
dependencies:
  flutter:
    sdk: flutter
  
  # ... 其他依赖 ...
  
  # JSON 序列化支持
  json_annotation: ^4.8.1
  # 堆叠式网格视图
  flutter_staggered_grid_view: ^0.7.0

验证:

  • ✅ 确认缩进正确(使用2个空格)
  • ✅ 确认版本号正确
  • ✅ 确认没有语法错误(冒号、引号等)

版本说明:

  • flutter_staggered_grid_view: ^0.7.0 - 堆叠式网格视图库

重要提示:

  • ⚠️ 如果版本冲突,可以运行 flutter pub upgrade 升级所有依赖
步骤 1.3:保存文件

操作说明:

  1. 💾 保存 pubspec.yaml 文件(Ctrl+S 或 Cmd+S)
  2. ✅ 确认文件已保存

⬇️ 步骤 2:安装依赖

操作步骤:

  1. 📂 打开终端(Terminal),切换到项目根目录

    cd /path/to/your/project
    
  2. ⌨️ 执行以下命令:

    flutter pub get
    
  3. ⏳ 等待安装完成(可能需要10-30秒)

命令说明:

  • flutter pub get - 下载并安装所有在 pubspec.yaml 中声明的依赖包
  • 安装成功后,依赖包会被下载到项目的 .dart_tool 目录

预期输出:

Running "flutter pub get" in ffohnotes...
Resolving dependencies...
Got dependencies!

验证:

  • ✅ 确认终端显示 “Got dependencies!”
  • ✅ 确认没有错误信息

✅ 步骤 3:验证安装

操作步骤:

  1. 📄 检查 pubspec.lock 文件,确认 flutter_staggered_grid_view 已安装

  2. 🔍 在代码中尝试导入库:

    import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
    
  3. ✅ 确认 IDE 没有报错(红色波浪线)

验证:

  • ✅ 确认 pubspec.lock 中包含 flutter_staggered_grid_view: 0.7.0
  • ✅ 确认导入语句没有错误

天气详情页面实战案例

📄 文件说明:lib/screens/weather_detail_page.dart

文件作用: 天气详情页面 UI 实现,使用堆叠式卡片展示多城市天气,支持滑动切换

flutter_staggered_grid_view 使用位置:

  1. 导入库:文件开头导入 flutter_staggered_grid_view
  2. 堆叠式卡片视图_buildStaggeredGridView() 方法中使用 Stack 和 PageView 实现堆叠效果
  3. 城市卡片构建_buildCityCard() 方法构建单个城市卡片

📋 多城市天气卡片切换实现流程图

左滑

右滑

🚀 页面启动

📥 initState: 加载城市列表

📋 从 SharedPreferences 读取城市

有保存的城市?

🌐 并发加载所有城市天气

➕ 添加默认城市

🎴 构建堆叠式卡片视图

📱 使用 Stack + PageView

👆 用户滑动卡片

滑动方向?

➡️ 切换到下一个城市

⬅️ 切换到上一个城市

🔄 更新当前城市索引

📊 更新UI显示

↩️ 用户点击撤销按钮

⬅️ 回到上一个城市

🎯 天气详情页面实现的功能模块

1. 📦 添加导入语句

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 在文件开头添加导入语句:
// 导入 Flutter Material 设计库
import 'package:flutter/material.dart';
// 导入 http HTTP 客户端
import 'package:http/http.dart' as http;
// 导入 flutter_staggered_grid_view 用于堆叠式卡片展示
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
// 导入 shared_preferences 用于读取城市列表
import 'package:shared_preferences/shared_preferences.dart';
// 导入数据模型
import '../models/weather_models.dart';
// 导入 JSON 序列化
import 'dart:convert';

验证:

  • ✅ 确认所有导入语句正确
  • ✅ 确认没有导入错误
2. 🔧 添加状态变量

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 在 _WeatherDetailPageState 类中添加以下状态变量:
class _WeatherDetailPageState extends State<WeatherDetailPage> {
  // API 密钥(实际使用时应该从配置文件或环境变量读取)
  static const String _apiKey = '和风天气控制台获取API KEY';
  
  // API 基础地址
  static const String _baseUrl = '和风天气控制台获取API HOST';
  
  // PageController 用于控制卡片滑动
  late PageController _pageController;
  
  // 城市列表
  List<Location> _cities = [];
  
  // 当前选中的城市索引
  int _currentCityIndex = 0;
  
  // 城市天气数据映射(城市ID -> 天气数据)
  final Map<String, Map<String, dynamic>> _cityWeatherData = {};
  
  // 预报天数选项
  final List<Map<String, String>> _forecastDays = [
    {'value': '3d', 'label': '3天'},
    {'value': '7d', 'label': '7天'},
    {'value': '10d', 'label': '10天'},
    {'value': '15d', 'label': '15天'},
    {'value': '30d', 'label': '30天'},
  ];
  
  String _selectedDays = '7d'; // 默认7天
  
  // 数据状态
  bool _isLoading = false;
  String? _errorMessage;
  
  // 展开的预报项索引(按城市ID存储)
  final Map<String, Set<int>> _expandedIndices = {};
  
  // 滑动偏移量(用于堆叠效果)
  final Map<int, double> _cardOffsets = {};
}

关键点说明:

  1. PageController

    • _pageController - 控制 PageView 的滑动行为
    • 用于实现左右滑动切换城市
  2. 城市列表

    • _cities - 存储所有城市信息
    • _currentCityIndex - 当前选中的城市索引
  3. 天气数据映射

    • _cityWeatherData - 以城市ID为键,存储每个城市的天气数据
    • 提高数据访问效率

验证:

  • ✅ 确认所有状态变量已添加
  • ✅ 确认类型声明正确
3. 📥 实现加载城市列表方法

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 在 initState() 方法中初始化 PageController 并加载城市:
  
  void initState() {
    super.initState();
    _pageController = PageController(initialPage: 0);
    _loadCities();
  }
  
  
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 当页面重新显示时,检查城市列表是否有变化
    _checkAndReloadCities();
  }
  
  
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }
  
  /// 📥 加载城市列表
  /// 
  /// **功能说明:** 从 SharedPreferences 中读取已保存的城市列表
  Future<void> _loadCities() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final citiesJson = prefs.getString('saved_cities');
      
      if (citiesJson != null && citiesJson.isNotEmpty) {
        final List<dynamic> citiesList = jsonDecode(citiesJson);
        final cities = citiesList
            .map((json) => Location.fromJson(json as Map<String, dynamic>))
            .toList();
        
        // 如果没有保存的城市,添加默认城市
        if (cities.isEmpty) {
          cities.add(Location(
            name: '北京',
            id: '101010100',
            lat: '39.9042',
            lon: '116.4074',
            adm2: '北京',
            adm1: '北京市',
            country: '中国',
            tz: 'Asia/Shanghai',
            utcOffset: '+08:00',
            isDst: '0',
            type: 'city',
            rank: '10',
            fxLink: '',
          ));
        }
        
        setState(() {
          _cities = cities;
          _currentCityIndex = 0;
        });
        
        // 加载所有城市的天气数据
        await _loadAllCitiesWeather();
      } else {
        // 如果没有保存的城市,使用默认城市
        final defaultCity = Location(
          name: '北京',
          id: '101010100',
          lat: '39.9042',
          lon: '116.4074',
          adm2: '北京',
          adm1: '北京市',
          country: '中国',
          tz: 'Asia/Shanghai',
          utcOffset: '+08:00',
          isDst: '0',
          type: 'city',
          rank: '10',
          fxLink: '',
        );
        
        setState(() {
          _cities = [defaultCity];
          _currentCityIndex = 0;
        });
        
        await _loadAllCitiesWeather();
      }
    } catch (e) {
      debugPrint('加载城市列表失败: $e');
      // 使用默认城市
      final defaultCity = Location(
        name: '北京',
        id: '101010100',
        lat: '39.9042',
        lon: '116.4074',
        adm2: '北京',
        adm1: '北京市',
        country: '中国',
        tz: 'Asia/Shanghai',
        utcOffset: '+08:00',
        isDst: '0',
        type: 'city',
        rank: '10',
        fxLink: '',
      );
      
      setState(() {
        _cities = [defaultCity];
        _currentCityIndex = 0;
      });
      
      await _loadAllCitiesWeather();
    }
  }

关键点说明:

  1. PageController 初始化

    • PageController(initialPage: 0) - 初始页面为第一个城市
    • 必须在 dispose() 中释放资源
  2. 城市列表加载

    • 从 SharedPreferences 读取保存的城市列表
    • 如果没有城市,使用默认城市(北京)
  3. 错误处理

    • 捕获异常,使用默认城市作为降级方案

验证:

  • ✅ 确认 PageController 已初始化
  • ✅ 确认 didChangeDependencies 已添加
  • ✅ 确认城市列表加载逻辑正确
  • ✅ 确认错误处理已添加

image-20260127160505158

4. 🔍 实现检查并重新加载城市列表方法

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 添加 _checkAndReloadCities() 方法:
  /// 🔍 检查并重新加载城市列表
  /// 
  /// **功能说明:** 检查 SharedPreferences 中的城市列表是否有变化,如果有新城市添加,则重新加载
  Future<void> _checkAndReloadCities() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final citiesJson = prefs.getString('saved_cities');
      
      if (citiesJson == null || citiesJson.isEmpty) {
        // 如果没有保存的城市,且当前也没有城市,则加载默认城市
        if (_cities.isEmpty) {
          await _loadCities();
        }
        return;
      }
      
      final List<dynamic> citiesList = jsonDecode(citiesJson);
      final newCities = citiesList
          .map((json) => Location.fromJson(json as Map<String, dynamic>))
          .toList();
      
      // 如果没有保存的城市,添加默认城市
      if (newCities.isEmpty) {
        newCities.add(Location(
          name: '北京',
          id: '101010100',
          lat: '39.9042',
          lon: '116.4074',
          adm2: '北京',
          adm1: '北京市',
          country: '中国',
          tz: 'Asia/Shanghai',
          utcOffset: '+08:00',
          isDst: '0',
          type: 'city',
          rank: '10',
          fxLink: '',
        ));
      }
      
      // 比较城市列表是否有变化(通过城市ID列表比较)
      final currentCityIds = _cities.map((c) => c.id).toSet();
      final newCityIds = newCities.map((c) => c.id).toSet();
      
      // 检查是否有新城市添加
      final addedCityIds = newCityIds.difference(currentCityIds);
      // 检查是否有城市删除
      final removedCityIds = currentCityIds.difference(newCityIds);
      
      // 如果有城市添加或删除,重新加载
      if (addedCityIds.isNotEmpty || removedCityIds.isNotEmpty) {
        // 保存当前城市ID(如果当前城市还在列表中)
        final currentCityId = _cities.isNotEmpty && 
            _currentCityIndex < _cities.length 
            ? _cities[_currentCityIndex].id 
            : null;
        
        // 更新城市列表
        setState(() {
          _cities = newCities;
          
          // 如果当前城市还在列表中,保持索引;否则重置为0
          if (currentCityId != null && newCityIds.contains(currentCityId)) {
            final newIndex = newCities.indexWhere((c) => c.id == currentCityId);
            _currentCityIndex = newIndex >= 0 ? newIndex : 0;
          } else {
            _currentCityIndex = 0;
          }
        });
        
        // 更新 PageController 到新的索引
        if (_pageController.hasClients && _currentCityIndex < _cities.length) {
          _pageController.jumpToPage(_currentCityIndex);
        }
        
        // 如果有新城市添加,只加载新城市的天气数据;否则重新加载所有城市
        if (addedCityIds.isNotEmpty) {
          // 只加载新添加城市的天气数据
          await _loadMissingCitiesWeather(addedCityIds.toList());
          
          // 删除已移除城市的天气数据
          for (var removedId in removedCityIds) {
            _cityWeatherData.remove(removedId);
            _expandedIndices.remove(removedId);
          }
        } else {
          // 如果只是删除城市,重新加载所有城市的天气数据
          await _loadAllCitiesWeather();
        }
      } else {
        // 如果城市列表没有变化,只加载缺失城市的天气数据(如果有)
        final missingCityIds = newCityIds
            .where((id) => !_cityWeatherData.containsKey(id))
            .toList();
        
        if (missingCityIds.isNotEmpty) {
          // 只加载缺失城市的天气数据
          await _loadMissingCitiesWeather(missingCityIds);
        }
      }
    } catch (e) {
      debugPrint('检查城市列表失败: $e');
    }
  }
  
  /// 🌐 加载缺失城市的天气数据
  /// 
  /// **功能说明:** 只加载指定城市ID列表的天气数据
  Future<void> _loadMissingCitiesWeather(List<String> cityIds) async {
    try {
      final futures = cityIds.map((cityId) async {
        final city = _cities.firstWhere((c) => c.id == cityId);
        try {
          final results = await Future.wait([
            _getCurrentWeather(city.id),
            _getDailyForecast(city.id),
          ]);
          
          return {
            'cityId': city.id,
            'weather': results[0] as WeatherResponse,
            'forecast': results[1] as DailyForecastResponse,
          };
        } catch (e) {
          debugPrint('加载城市 ${city.name} 天气失败: $e');
          return null;
        }
      }).toList();
      
      final results = await Future.wait(futures);
      
      setState(() {
        for (var result in results) {
          if (result != null) {
            _cityWeatherData[result['cityId'] as String] = {
              'weather': result['weather'],
              'forecast': result['forecast'],
            };
          }
        }
      });
    } catch (e) {
      debugPrint('加载缺失城市天气失败: $e');
    }
  }

关键点说明:

  1. didChangeDependencies 生命周期

    • 当页面重新显示时自动调用
    • 用于检测城市管理页面添加的新城市
  2. 城市列表变化检测

    • 通过比较城市ID集合检测变化
    • addedCityIds - 新添加的城市ID
    • removedCityIds - 删除的城市ID
  3. 智能加载策略

    • 如果有新城市添加,只加载新城市的天气数据(优化性能)
    • 如果有城市删除,删除对应的天气数据
    • 如果城市列表没有变化但缺少天气数据,只加载缺失的
  4. 索引保持

    • 如果当前城市还在列表中,保持当前索引
    • 如果当前城市被删除,重置为第一个城市

image-20260127160547288

验证:

  • ✅ 确认 didChangeDependencies 已添加
  • ✅ 确认城市列表变化检测逻辑正确
  • ✅ 确认只加载新城市的天气数据(性能优化)
  • ✅ 确认索引保持逻辑正确
5. 🌐 实现加载所有城市天气数据方法

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 添加 _loadAllCitiesWeather() 方法:
  /// 🌐 加载所有城市的天气数据
  /// 
  /// **功能说明:** 并发加载所有城市的天气数据
  Future<void> _loadAllCitiesWeather() async {
    if (_cities.isEmpty) return;
    
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });
    
    try {
      // 并发加载所有城市的天气数据
      final futures = _cities.map((city) async {
        try {
          final results = await Future.wait([
            _getCurrentWeather(city.id),
            _getDailyForecast(city.id),
          ]);
          
          return {
            'cityId': city.id,
            'weather': results[0] as WeatherResponse,
            'forecast': results[1] as DailyForecastResponse,
          };
        } catch (e) {
          debugPrint('加载城市 ${city.name} 天气失败: $e');
          return null;
        }
      }).toList();
      
      final results = await Future.wait(futures);
      
      setState(() {
        for (var result in results) {
          if (result != null) {
            _cityWeatherData[result['cityId'] as String] = {
              'weather': result['weather'],
              'forecast': result['forecast'],
            };
          }
        }
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = '加载失败: $e';
        _isLoading = false;
      });
    }
  }

关键点说明:

  1. 并发加载

    • 使用 Future.wait() 并发加载所有城市的天气数据
    • 提高加载效率,减少等待时间
  2. 数据存储

    • 使用 Map 存储每个城市的天气数据
    • 以城市ID为键,方便快速访问
  3. 错误处理

    • 单个城市加载失败不影响其他城市
    • 使用 debugPrint 记录错误信息

验证:

  • ✅ 确认使用了并发加载
  • ✅ 确认数据存储结构正确
  • ✅ 确认错误处理已添加

image-20260127160634421

6. 🎴 实现堆叠式卡片视图

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 添加 _buildStaggeredGridView() 方法:
  /// 🎴 构建堆叠式卡片视图
  /// 
  /// **功能说明:** 使用 Stack 和 PageView 实现堆叠式多城市天气卡片切换
  Widget _buildStaggeredGridView() {
    if (_cities.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.location_off,
              size: 64,
              color: Colors.grey.shade300,
            ),
            const SizedBox(height: 16),
            Text(
              '暂无城市',
              style: TextStyle(
                fontSize: 16,
                color: Colors.grey.shade600,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              '请在"城市管理"中添加城市',
              style: TextStyle(
                fontSize: 14,
                color: Colors.grey.shade500,
              ),
            ),
          ],
        ),
      );
    }
    
    return Stack(
      children: [
        // 背景卡片(堆叠效果)
        ...List.generate(
          _cities.length,
          (index) {
            if (index >= _currentCityIndex || index < _currentCityIndex - 2) {
              return const SizedBox.shrink();
            }
            
            final offset = (_currentCityIndex - index) * 8.0;
            final scale = 1.0 - (index - _currentCityIndex).abs() * 0.05;
            final opacity = 1.0 - (index - _currentCityIndex).abs() * 0.3;
            
            return Positioned.fill(
              top: offset,
              left: offset,
              right: -offset,
              bottom: -offset,
              child: Transform.scale(
                scale: scale.clamp(0.9, 1.0),
                child: Opacity(
                  opacity: opacity.clamp(0.3, 1.0),
                  child: _buildCityCard(index, isActive: false),
                ),
              ),
            );
          },
        ),
        // 当前卡片(PageView)
        PageView.builder(
          controller: _pageController,
          onPageChanged: (index) {
            setState(() {
              _currentCityIndex = index;
            });
          },
          itemCount: _cities.length,
          itemBuilder: (context, index) {
            return _buildCityCard(index, isActive: true);
          },
        ),
      ],
    );
  }

关键点说明:

  1. Stack 布局

    • 使用 Stack 实现堆叠效果
    • 背景卡片使用 Positioned.fill 定位
  2. 堆叠效果

    • offset - 卡片偏移量,实现堆叠视觉
    • scale - 卡片缩放比例,后面的卡片更小
    • opacity - 卡片透明度,后面的卡片更透明
  3. PageView

    • PageView.builder - 实现左右滑动切换
    • onPageChanged - 滑动回调,更新当前城市索引

验证:

  • ✅ 确认 Stack 布局正确
  • ✅ 确认堆叠效果已实现
  • ✅ 确认 PageView 滑动功能正常

image-20260127160729368

7. 🏙️ 实现单个城市卡片构建方法

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 添加 _buildCityCard() 方法:
  /// 🏙️ 构建单个城市卡片
  /// 
  /// **功能说明:** 构建单个城市的天气详情卡片
  Widget _buildCityCard(int index, {required bool isActive}) {
    if (index >= _cities.length) {
      return const SizedBox.shrink();
    }
    
    final city = _cities[index];
    final weatherData = _cityWeatherData[city.id];
    
    if (weatherData == null) {
      return Container(
        margin: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(20),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.04),
              blurRadius: 10,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6366F1)),
              ),
              const SizedBox(height: 16),
              Text(
                '加载 ${city.name} 天气中...',
                style: TextStyle(
                  fontSize: 14,
                  color: Colors.grey.shade600,
                ),
              ),
            ],
          ),
        ),
      );
    }
    
    final weather = weatherData['weather'] as WeatherResponse;
    final forecast = weatherData['forecast'] as DailyForecastResponse;
    
    return RefreshIndicator(
      onRefresh: () async {
        if (index == _currentCityIndex) {
          await _refreshCurrentCityWeather();
        } else {
          final city = _cities[index];
          try {
            final results = await Future.wait([
              _getCurrentWeather(city.id),
              _getDailyForecast(city.id),
            ]);
            
            setState(() {
              _cityWeatherData[city.id] = {
                'weather': results[0] as WeatherResponse,
                'forecast': results[1] as DailyForecastResponse,
              };
            });
          } catch (e) {
            debugPrint('刷新城市 ${city.name} 天气失败: $e');
          }
        }
      },
      color: const Color(0xFF6366F1),
      child: SingleChildScrollView(
        physics: const AlwaysScrollableScrollPhysics(),
        child: Column(
          children: [
            const SizedBox(height: 8),
            
            // 当前天气卡片 - 简约风格
            if (weather.now != null)
              _buildCurrentWeatherCard(weather.now!, city),
            
            const SizedBox(height: 16),
            
            // 预报天数选择器 - 简约风格
            _buildForecastDaysSelector(),
            
            const SizedBox(height: 16),
            
            // 天气预报列表 - 简约风格
            if (forecast.daily != null)
              _buildForecastList(forecast.daily!, city.id),
            
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

关键点说明:

  1. 卡片状态

    • isActive - 标识卡片是否为当前激活状态
    • 用于区分当前卡片和背景卡片
  2. 数据加载

    • 如果数据未加载,显示加载指示器
    • 如果数据已加载,显示天气详情
  3. 下拉刷新

    • 使用 RefreshIndicator 实现下拉刷新
    • 支持刷新当前城市或其他城市的天气数据

验证:

  • ✅ 确认卡片构建逻辑正确
  • ✅ 确认加载状态已处理
  • ✅ 确认下拉刷新功能正常
8. 🔄 实现切换城市和撤销滑动方法

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 添加切换城市和撤销滑动方法:
  /// 🔄 切换城市
  /// 
  /// **功能说明:** 切换到指定索引的城市
  void _switchCity(int index) {
    if (index >= 0 && index < _cities.length && index != _currentCityIndex) {
      setState(() {
        _currentCityIndex = index;
      });
      _pageController.animateToPage(
        index,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    }
  }
  
  /// ↩️ 撤销滑动
  /// 
  /// **功能说明:** 撤销当前滑动,回到上一个城市
  void _undoSwipe() {
    if (_currentCityIndex > 0) {
      _switchCity(_currentCityIndex - 1);
    }
  }

关键点说明:

  1. 切换城市

    • _switchCity() - 切换到指定索引的城市
    • 使用 PageController.animateToPage() 实现动画切换
  2. 撤销滑动

    • _undoSwipe() - 回到上一个城市
    • 提供用户友好的撤销功能

验证:

  • ✅ 确认切换城市功能正常
  • ✅ 确认撤销滑动功能正常
  • ✅ 确认动画效果流畅
9. 🎨 更新 AppBar 添加撤销按钮

文件路径: lib/screens/weather_detail_page.dart

操作步骤:

  1. 📝 修改 AppBar,添加撤销按钮:
      appBar: AppBar(
        title: Text(
          _cities.isNotEmpty && _currentCityIndex < _cities.length
              ? _cities[_currentCityIndex].name
              : '天气详情',
          style: const TextStyle(
            fontWeight: FontWeight.w600,
            fontSize: 18,
            color: Colors.black87,
          ),
        ),
        backgroundColor: Colors.white,
        elevation: 0,
        centerTitle: true,
        actions: [
          // 撤销滑动按钮
          if (_currentCityIndex > 0)
            IconButton(
              icon: Icon(Icons.undo, color: Colors.grey.shade700),
              onPressed: _undoSwipe,
              tooltip: '撤销滑动',
            ),
          // 刷新按钮
          IconButton(
            icon: Icon(Icons.refresh, color: Colors.grey.shade700),
            onPressed: _refreshCurrentCityWeather,
            tooltip: '刷新',
          ),
        ],
      ),

关键点说明:

  1. 动态标题

    • 显示当前选中城市的名称
    • 如果没有城市,显示默认标题
  2. 撤销按钮

    • 只在不是第一个城市时显示
    • 点击后回到上一个城市

验证:

  • ✅ 确认 AppBar 标题动态更新
  • ✅ 确认撤销按钮显示逻辑正确
  • ✅ 确认按钮功能正常

🎨 UI 设计要点

  • 🎨 堆叠式展示:使用 Stack 和 Positioned 实现卡片堆叠效果
  • 👆 滑动切换:使用 PageView 实现左右滑动切换城市
  • 📱 响应式布局:适配不同屏幕尺寸,使用 SingleChildScrollView
  • ⏳ 加载状态:显示紫色加载指示器和友好的加载提示
  • ❌ 错误处理:友好的错误提示和重试机制
  • 🎯 交互反馈:滑动动画、撤销功能、下拉刷新

常见错误及解决方案

🔧 错误处理流程图

导入错误

PageController错误

滑动不流畅

堆叠效果异常

数据加载失败

⚠️ 遇到错误

错误类型?

📦 检查依赖安装

🔧 检查初始化

⚙️ 检查动画配置

📐 检查偏移量计算

🌐 检查网络请求

✅ 问题解决

错误 1:导入错误

错误信息:

Error: Could not find a file named "package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart"

原因分析:

  • 依赖未正确安装
  • pubspec.yaml 配置错误

解决方案:

  1. 检查 pubspec.yaml 中的依赖配置:

    dependencies:
      flutter_staggered_grid_view: ^0.7.0
    
  2. 重新安装依赖:

    flutter pub get
    
  3. 如果仍然失败,清理项目后重新安装:

    flutter clean
    flutter pub get
    

错误 2:PageController 未初始化

错误信息:

LateInitializationError: Field '_pageController' has not been initialized

原因分析:

  • PageController 未在 initState() 中初始化
  • 在初始化前使用了 PageController

解决方案:

  1. 确保在 initState() 中初始化:

    
    void initState() {
      super.initState();
      _pageController = PageController(initialPage: 0);
      _loadCities();
    }
    
  2. 确保在 dispose() 中释放资源:

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

错误 3:滑动不流畅

错误信息:

  • 滑动卡顿
  • 动画不流畅

原因分析:

  • 动画时长设置不当
  • 堆叠效果计算过于复杂

解决方案:

  1. 优化动画配置:

    _pageController.animateToPage(
      index,
      duration: const Duration(milliseconds: 300),  // 300ms 动画时长
      curve: Curves.easeInOut,  // 缓动曲线
    );
    
  2. 减少堆叠卡片数量:

    // 只显示当前卡片前后各2张卡片
    if (index >= _currentCityIndex || index < _currentCityIndex - 2) {
      return const SizedBox.shrink();
    }
    

错误 4:堆叠效果异常

错误信息:

  • 卡片重叠不正确
  • 偏移量计算错误

原因分析:

  • 偏移量计算公式错误
  • 缩放比例计算错误

解决方案:

  1. 检查偏移量计算:

    final offset = (_currentCityIndex - index) * 8.0;  // 每张卡片偏移8像素
    
  2. 检查缩放比例:

    final scale = 1.0 - (index - _currentCityIndex).abs() * 0.05;
    scale: scale.clamp(0.9, 1.0),  // 限制在0.9到1.0之间
    
  3. 检查透明度:

    final opacity = 1.0 - (index - _currentCityIndex).abs() * 0.3;
    opacity: opacity.clamp(0.3, 1.0),  // 限制在0.3到1.0之间
    

错误 5:数据加载失败

错误信息:

Exception: 加载失败

原因分析:

  • 网络请求失败
  • API 密钥错误
  • 城市数据格式错误

解决方案:

  1. 检查网络连接

  2. 检查 API 密钥配置

  3. 检查城市数据格式:

    final List<dynamic> citiesList = jsonDecode(citiesJson);
    final cities = citiesList
        .map((json) => Location.fromJson(json as Map<String, dynamic>))
        .toList();
    
  4. 添加错误处理:

    try {
      // 加载数据
    } catch (e) {
      debugPrint('加载失败: $e');
      // 显示错误提示
    }
    

总结

📊 完整实现流程图

🚀 项目开始

📦 步骤1: 引入三方库

📄 1.1: 添加 flutter_staggered_grid_view 到 pubspec.yaml

⬇️ 1.2: 运行 flutter pub get

✅ 1.3: 验证安装

📝 步骤2: 修改天气详情页面

📥 2.1: 添加导入语句

🔧 2.2: 添加状态变量

📥 2.3: 实现加载城市列表方法

🔍 2.4: 实现检查并重新加载城市列表方法

🌐 2.5: 实现加载所有城市天气数据方法

🎴 2.6: 实现堆叠式卡片视图

🏙️ 2.7: 实现单个城市卡片构建方法

🔄 2.8: 实现切换城市和撤销滑动方法

🎨 2.9: 更新 AppBar 添加撤销按钮

✅ 项目完成

📋 本教程完成的内容

本教程详细介绍了如何在 Flutter 项目天气详情页面中使用 flutter_staggered_grid_view 实现多城市天气卡片堆叠式展示和滑动切换功能。主要内容包括:

  1. 📦 引入三方库:添加 flutter_staggered_grid_view 依赖到 pubspec.yaml
  2. 🌤️ 天气详情页面实现:在天气详情页面中实现多城市卡片切换
    • 📥 加载城市列表(从 SharedPreferences 读取)
    • 🔍 检查并重新加载城市列表(同步城市管理页面的新增城市)
    • 🌐 并发加载所有城市天气数据
    • 🎴 堆叠式卡片视图(使用 Stack + PageView)
    • 🏙️ 单个城市卡片构建
    • 🔄 切换城市和撤销滑动功能
    • 🎨 AppBar 撤销按钮
  3. 🔧 错误处理:提供常见错误及解决方案,帮助新手快速解决问题

💡 关键要点

  • 🎴 堆叠式展示:使用 Stack 和 Positioned 实现卡片堆叠效果
  • 👆 滑动切换:使用 PageView 实现左右滑动切换城市
  • 📊 数据管理:使用 Map 存储每个城市的天气数据,提高访问效率
  • 🔄 并发加载:使用 Future.wait() 并发加载所有城市天气数据
  • 🔍 城市同步:使用 didChangeDependencies 自动检测城市管理页面新增的城市
  • 智能加载:只加载新添加城市的天气数据,优化性能
  • ↩️ 撤销功能:提供撤销滑动功能,提升用户体验
  • 🛡️ 错误处理:完善的错误处理机制,确保应用稳定性

📚 参考资源

🎉 祝你开发顺利! 🚀
欢迎加入开源鸿蒙跨平台社区

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐