【Harmonyos】开源鸿蒙跨平台训练营DAY10: 获取特惠推荐数据

Flutter鸿蒙开发指南(十):获取特惠推荐数据(AI)


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

前言

在电商应用中,特惠推荐模块是吸引用户点击、提升转化率的重要功能。本文将详细介绍如何在 Flutter/HarmonyOS 项目中实现特惠推荐数据的获取与渲染功能,完整展示从数据模型构建到 UI 展示的开发流程,并记录开发过程中遇到的问题及解决方案。

一、实现流程

按照规范的六步开发流程:

复制代码
(1)定义接口常量 - 添加特惠推荐接口地址
(2)构建数据模型 - 根据JSON生成数据模型类
(3)封装API调用 - 实现数据获取接口
(4)首页数据初始化 - 在组件中获取数据
(5)传递数据到子组件 - 更新UI组件接收参数
(6)完善UI展示 - 实现商品列表渲染

1.1 特惠推荐接口介绍

接口地址https://meikou-api.itheima.net/hot/preference

返回数据结构

json 复制代码
{
  "code": "1",
  "msg": "操作成功",
  "result": {
    "id": "897682543",
    "title": "特惠推荐",
    "subTypes": [
      {
        "id": "912000341",
        "title": "抢先尝鲜",
        "goodsItems": {
          "counts": 459,
          "pageSize": 10,
          "pages": 46,
          "page": 1,
          "items": [
            {
              "id": "1750713979950333956",
              "name": "Balva 日本制高级时尚太阳镜 方框",
              "desc": "抵挡99%紫外线太阳镜",
              "price": "1213.00",
              "picture": "https://...",
              "orderNum": 17
            }
          ]
        }
      }
    ]
  }
}

二、代码实现

2.1 定义接口常量

文件lib/constants/index.dart

dart 复制代码
// 存放请求地址接口的常量
class HttpConstants {
  // 轮播图接口
  static const String BANNER_LIST = "/home/banner";

  // 分类列表接口
  static const String CATEGORY_LIST = "/home/category/head";

  // 特惠推荐地址
  static const String PRODUCT_LIST = "/hot/preference";
}

2.2 构建数据模型

文件lib/viewmodels/home.dart

根据 JSON 数据结构,我们需要创建四个数据模型类:

dart 复制代码
// 商品项
class GoodsItem {
  String id;
  String name;
  String? desc;
  String price;
  String picture;
  int orderNum;

  GoodsItem({
    required this.id,
    required this.name,
    this.desc,
    required this.price,
    required this.picture,
    required this.orderNum,
  });

  factory GoodsItem.fromJSON(Map<String, dynamic> json) {
    return GoodsItem(
      id: json["id"] ?? "",
      name: json["name"] ?? "",
      desc: json["desc"],
      price: json["price"] ?? "",
      picture: json["picture"] ?? "",
      orderNum: json["orderNum"] ?? 0,
    );
  }
}

// 商品列表
class GoodsItems {
  int counts;
  int pageSize;
  int pages;
  int page;
  List<GoodsItem> items;

  GoodsItems({
    required this.counts,
    required this.pageSize,
    required this.pages,
    required this.page,
    required this.items,
  });

  factory GoodsItems.fromJSON(Map<String, dynamic> json) {
    return GoodsItems(
      counts: json["counts"] ?? 0,
      pageSize: json["pageSize"] ?? 0,
      pages: json["pages"] ?? 0,
      page: json["page"] ?? 0,
      items: (json["items"] as List?)
          ?.map((item) => GoodsItem.fromJSON(item as Map<String, dynamic>))
          .toList() ?? [],
    );
  }
}

// 子类型
class SubType {
  String id;
  String title;
  GoodsItems goodsItems;

  SubType({
    required this.id,
    required this.title,
    required this.goodsItems,
  });

  factory SubType.fromJSON(Map<String, dynamic> json) {
    return SubType(
      id: json["id"] ?? "",
      title: json["title"] ?? "",
      goodsItems: GoodsItems.fromJSON(json["goodsItems"] ?? {}),
    );
  }
}

// 特惠推荐结果
class SpecialOfferResult {
  String id;
  String title;
  List<SubType> subTypes;

  SpecialOfferResult({
    required this.id,
    required this.title,
    required this.subTypes,
  });

  factory SpecialOfferResult.fromJSON(Map<String, dynamic> json) {
    return SpecialOfferResult(
      id: json["id"] ?? "",
      title: json["title"] ?? "",
      subTypes: (json["subTypes"] as List?)
          ?.map((item) => SubType.fromJSON(item as Map<String, dynamic>))
          .toList() ?? [],
    );
  }
}

关键点

  • 使用 factory 关键字声明工厂函数
  • 使用空安全操作符 ?? 提供默认值
  • desc 字段使用 String? 可空类型(API 可能返回 null)
  • items 使用可空列表处理 as List?

2.3 封装 API 调用

文件lib/api/home.dart

dart 复制代码
/// 获取特惠推荐数据
Future<SpecialOfferResult> getProductListAPI() async {
  // 返回请求
  return SpecialOfferResult.fromJSON(
      await dioRequest.get(HttpConstants.PRODUCT_LIST));
}

2.4 首页数据初始化

文件lib/pages/home/index.dart

dart 复制代码
class _HomeViewState extends State<HomeView> {
  // 分类列表
  List<CategoryItem> _categoryList = [];

  // 轮播图列表
  List<BannerItem> _bannerList = [];

  // 特惠推荐
  SpecialOfferResult _specialOfferResult = SpecialOfferResult(
    id: "",
    title: "",
    subTypes: [],
  );

  @override
  void initState() {
    super.initState();
    _getBannederList();
    _getCategoryList();
    _getProductList();  // 获取特惠推荐数据
  }

  // 获取特惠推荐
  void _getProductList() async {
    try {
      _specialOfferResult = await getProductListAPI();
      setState(() {});
    } catch (e) {
      print('获取特惠推荐数据失败: $e');
    }
  }

  // 在滚动视图中使用
  List<Widget> _getScrollChildren() {
    return [
      SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)),
      const SliverToBoxAdapter(child: SizedBox(height: 10)),
      SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)),
      const SliverToBoxAdapter(child: SizedBox(height: 10)),
      SliverToBoxAdapter(
          child: HmSuggestion(specialOfferResult: _specialOfferResult)), // 推荐组件
      // ...
    ];
  }
}

2.5 更新特惠推荐组件

文件lib/components/Home/HmSuggestion.dart

dart 复制代码
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';

class HmSuggestion extends StatefulWidget {
  // 父传子
  final SpecialOfferResult specialOfferResult;

  const HmSuggestion({super.key, required this.specialOfferResult});

  @override
  State<HmSuggestion> createState() => _HmSuggestionState();
}

class _HmSuggestionState extends State<HmSuggestion> {
  // 取前三条数据
  List<GoodsItem> _getDisplayItems() {
    // 在初始化还没获取到数据直接返回空列表
    if (widget.specialOfferResult.subTypes.isEmpty) {
      return [];
    }

    return widget.specialOfferResult.subTypes.first.goodsItems.items
        .take(3)
        .toList();
  }

  Widget _buildHeader() {
    return Row(
      children: [
        const Text(
          "特惠推荐",
          style: TextStyle(
              color: Color.fromARGB(255, 86, 24, 20),
              fontSize: 18,
              fontWeight: FontWeight.w700),
        ),
        const SizedBox(width: 10),
        Text(
          widget.specialOfferResult.title.isNotEmpty
              ? widget.specialOfferResult.title
              : "精选省攻略",
          style: const TextStyle(
            fontSize: 12,
            color: Color.fromARGB(255, 124, 63, 58),
          ),
        ),
      ],
    );
  }

  // 左侧结构 - 渐变背景
  Widget _buildLeft() {
    return Container(
      width: 75,
      height: 140,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Color.fromARGB(255, 255, 120, 50),  // 橙红
            Color.fromARGB(255, 255, 180, 60),  // 橙黄
          ],
        ),
        boxShadow: [
          BoxShadow(
            color: const Color.fromARGB(255, 255, 140, 50).withOpacity(0.3),
            blurRadius: 8,
            offset: const Offset(2, 4),
          ),
        ],
      ),
      child: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.card_giftcard, color: Colors.white, size: 36),
            SizedBox(height: 8),
            Text(
              "特惠",
              style: TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 4),
            Text(
              "限时",
              style: TextStyle(
                color: Colors.white,
                fontSize: 11,
              ),
            ),
          ],
        ),
      ),
    );
  }

  // 构建商品图片
  Widget _buildProductImage(String imageUrl) {
    // 检查URL是否为空
    if (imageUrl.isEmpty) {
      return _buildErrorImage();
    }

    // 将 http:// 转换为 https:// 以解决明文流量问题
    String secureUrl = imageUrl;
    if (secureUrl.startsWith('http://')) {
      secureUrl = secureUrl.replaceFirst('http://', 'https://');
    }

    return SizedBox(
      width: double.infinity,
      height: 140,
      child: Image.network(
        secureUrl,
        fit: BoxFit.cover,
        loadingBuilder: (context, child, loadingProgress) {
          if (loadingProgress == null) return child;
          return _buildLoadingImage(loadingProgress);
        },
        errorBuilder: (context, error, stackTrace) {
          // 如果 https 失败,尝试用原 url (http)
          if (secureUrl != imageUrl) {
            return SizedBox(
              width: double.infinity,
              height: 140,
              child: Image.network(
                imageUrl,
                fit: BoxFit.cover,
                errorBuilder: (context, error, stackTrace) {
                  return _buildErrorImage();
                },
              ),
            );
          }
          return _buildErrorImage();
        },
      ),
    );
  }

  // 加载中图片
  Widget _buildLoadingImage(ImageChunkEvent loadingProgress) {
    return Container(
      width: double.infinity,
      height: 140,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: Colors.grey[200],
      ),
      child: Center(
        child: CircularProgressIndicator(
          value: loadingProgress.expectedTotalBytes != null
              ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
              : null,
          strokeWidth: 2,
        ),
      ),
    );
  }

  // 错误图片占位符
  Widget _buildErrorImage() {
    return Container(
      width: double.infinity,
      height: 140,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: Colors.grey[300],
      ),
      child: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.image_not_supported, size: 32, color: Colors.grey),
            SizedBox(height: 4),
            Text(
              "图片加载失败",
              style: TextStyle(fontSize: 10, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }

  List<Widget> _getChildrenList() {
    List<GoodsItem> list = _getDisplayItems();
    return List.generate(list.length, (int index) {
      return Expanded(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4.0),
          child: Column(
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: _buildProductImage(list[index].picture),
              ),
              const SizedBox(height: 10),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(12),
                  color: const Color.fromARGB(255, 240, 96, 12),
                ),
                child: Text(
                  "¥${list[index].price}",
                  style: const TextStyle(color: Colors.white, fontSize: 11),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              )
            ],
          ),
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 10),
        child: Container(
            alignment: Alignment.center,
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: const Color.fromARGB(255, 255, 246, 238),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Column(
              children: [
                _buildHeader(),
                const SizedBox(height: 10),
                Row(
                  children: [
                    _buildLeft(),
                    const SizedBox(width: 4),
                    Expanded(
                        child: Row(
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: _getChildrenList()))
                  ],
                )
              ],
            )));
  }
}

三、遇到问题及解决方法

问题1:空数据导致崩溃

错误现象

复制代码
RangeError: RangeError (index): Invalid value: Valid value range is empty: 0

原因分析 :在数据还没加载完成时,subTypes 为空列表,直接访问 first 会导致崩溃。

解决方法 :在 _getDisplayItems() 中添加空数据检查

dart 复制代码
List<GoodsItem> _getDisplayItems() {
  // 在初始化还没获取到数据直接返回空列表
  if (widget.specialOfferResult.subTypes.isEmpty) {
    return [];
  }

  return widget.specialOfferResult.subTypes.first.goodsItems.items
      .take(3)
      .toList();
}

问题2:HTTP 图片加载失败

错误现象:某些商品图片无法显示,控制台无明确错误。

原因分析

  1. API 返回的某些图片 URL 使用 http:// 而非 https://
  2. HarmonyOS 默认禁止明文 HTTP 流量

解决方法 :自动将 http:// 转换为 https://

dart 复制代码
Widget _buildProductImage(String imageUrl) {
  if (imageUrl.isEmpty) {
    return _buildErrorImage();
  }

  // 将 http:// 转换为 https://
  String secureUrl = imageUrl;
  if (secureUrl.startsWith('http://')) {
    secureUrl = secureUrl.replaceFirst('http://', 'https://');
  }

  return Image.network(
    secureUrl,
    errorBuilder: (context, error, stackTrace) {
      // 如果 HTTPS 失败,尝试原 HTTP URL
      if (secureUrl != imageUrl) {
        return Image.network(imageUrl, ...);
      }
      return _buildErrorImage();
    },
  );
}

问题3:配置文件结构错误

错误现象

复制代码
hvigor ERROR: 00303038 Configuration Error
Schema validate failed, module.json5
property 'network' must be valid

原因分析 :尝试在 module.json5 根级别添加 network 配置,但 JSON Schema 不允许。

解决方法:使用纯代码解决方案,通过自动转换 URL 处理 HTTP 问题,而非依赖配置文件。

问题4:图片加载无反馈

错误现象:网络慢时,图片区域长时间空白。

解决方法 :添加 loadingBuilder 显示加载进度

dart 复制代码
Image.network(
  secureUrl,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return _buildLoadingImage(loadingProgress);
  },
)

Widget _buildLoadingImage(ImageChunkEvent loadingProgress) {
  return Container(
    child: Center(
      child: CircularProgressIndicator(
        value: loadingProgress.expectedTotalBytes != null
            ? loadingProgress.cumulativeBytesLoaded /
               loadingProgress.expectedTotalBytes!
            : null,
      ),
    ),
  );
}

问题5:desc 字段可能为 null

错误现象

复制代码
type 'Null' is not a subtype of type 'String'

原因分析 :API 返回的商品数据中 desc 字段可能为 null

解决方法 :使用可空类型 String?

dart 复制代码
class GoodsItem {
  String? desc;  // 使用可空类型
  // ...
}

四、效果展示

特惠推荐模块展示效果:

复制代码
┌─────────────────────────────────────────────────────────────┐
│  特惠推荐  精选省攻略                                          │
│  ┌──────────┐  ┌─────┐ ┌─────┐ ┌─────┐                     │
│  │          │  │图  │ │图  │ │图  │                     │
│  │   🎁     │  │片  │ │片  │ │片  │                     │
│  │  特惠    │  │ ¥xx │ ¥xx │ ¥xx │                     │
│  │  限时    │  │     │ │     │ │     │                     │
│  │          │  └─────┘ └─────┘ └─────┘                     │
│  └──────────┘                                               │
└─────────────────────────────────────────────────────────────┘

UI 特点

  • 左侧:橙红到橙黄渐变背景 + 礼品图标 + 双层文字
  • 右侧:前3条商品卡片,每张显示图片和价格
  • 背景:浅米白色容器,圆角设计
  • 阴影:柔和的橙色光晕效果

五、总结

本文介绍了 Flutter/HarmonyOS 电商应用中特惠推荐功能的完整实现流程:

  1. 数据模型:四层嵌套结构(GoodsItem → GoodsItems → SubType → SpecialOfferResult)
  2. 网络请求:封装 API 调用,统一处理业务状态码
  3. UI 渲染:渐变背景、图片加载优化、错误处理
  4. 问题解决:空数据检查、HTTP/HTTPS 兼容、加载进度显示

通过遵循规范的六步开发流程,可以快速实现类似的数据获取与展示功能。

源码地址

https://atomgit.com/lbbxmx111/haromyos_day_four

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

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
Yeats_Liao4 小时前
压力测试实战:基于Locust的高并发场景稳定性验证
人工智能·深度学习·机器学习·华为·开源·压力测试
一起养小猫4 小时前
Flutter for OpenHarmony 实战:网络请求与JSON解析完全指南
网络·jvm·spring·flutter·json·harmonyos
摘星编程4 小时前
React Native鸿蒙:DeviceInfo应用版本读取
react native·react.js·harmonyos
爱吃大芒果4 小时前
Flutter for OpenHarmony实战 : mango_shop API 客户端的封装与鸿蒙网络权限适配
网络·flutter·harmonyos
相思难忘成疾4 小时前
华为HCIP:MPLS实验
网络·华为·智能路由器·hcip
2601_949593654 小时前
高级进阶 React Native 鸿蒙跨平台开发:SVG 路径描边动画
react native·react.js·harmonyos
2501_920931704 小时前
React Native鸿蒙跨平台完成剧本杀组队消息与快捷入口组件技术解读,采用左侧图标+中间入口名称+右侧状态标签的设计实现快捷入口组件
javascript·react native·react.js·harmonyos
前端不太难5 小时前
HarmonyOS 上,App、游戏、PC 能共用架构吗?
游戏·架构·harmonyos
Swift社区5 小时前
HarmonyOS 应用开发环境搭建与 DevEco Studio 配置
华为·harmonyos