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

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

- Flutter鸿蒙开发指南(十):获取特惠推荐数据(AI)
-
- 前言
- 一、实现流程
-
- [1.1 特惠推荐接口介绍](#1.1 特惠推荐接口介绍)
- 二、代码实现
-
- [2.1 定义接口常量](#2.1 定义接口常量)
- [2.2 构建数据模型](#2.2 构建数据模型)
- [2.3 封装 API 调用](#2.3 封装 API 调用)
- [2.4 首页数据初始化](#2.4 首页数据初始化)
- [2.5 更新特惠推荐组件](#2.5 更新特惠推荐组件)
- 三、遇到问题及解决方法
-
- 问题1:空数据导致崩溃
- [问题2:HTTP 图片加载失败](#问题2:HTTP 图片加载失败)
- 问题3:配置文件结构错误
- 问题4:图片加载无反馈
- [问题5:desc 字段可能为 null](#问题5:desc 字段可能为 null)
- 四、效果展示
- 五、总结
- 源码地址
前言
在电商应用中,特惠推荐模块是吸引用户点击、提升转化率的重要功能。本文将详细介绍如何在 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 图片加载失败
错误现象:某些商品图片无法显示,控制台无明确错误。
原因分析:
- API 返回的某些图片 URL 使用
http://而非https:// - 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 电商应用中特惠推荐功能的完整实现流程:
- 数据模型:四层嵌套结构(GoodsItem → GoodsItems → SubType → SpecialOfferResult)
- 网络请求:封装 API 调用,统一处理业务状态码
- UI 渲染:渐变背景、图片加载优化、错误处理
- 问题解决:空数据检查、HTTP/HTTPS 兼容、加载进度显示
通过遵循规范的六步开发流程,可以快速实现类似的数据获取与展示功能。
源码地址
https://atomgit.com/lbbxmx111/haromyos_day_four
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
