【OpenHarmonyOS】DAY10:Flutter 鸿蒙电商开发:特惠推荐模块完整实现
四层嵌套模型图片优化:HTTP/HTTPS 自动转换 + 加载进度 + 错误占位空安全处理:可空类型 + 默认值兜底渐变设计营造促销氛围。
·
Flutter 鸿蒙电商开发:特惠推荐模块完整实现

摘要:本文详细介绍在 Flutter/HarmonyOS 项目中实现特惠推荐模块的完整流程。该模块涉及四层嵌套数据结构的解析、渐变 UI 设计、图片加载优化以及 HTTP/HTTPS 兼容处理,是电商应用中提升转化率的核心功能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、功能分析
1.1 业务价值
特惠推荐模块是电商应用的流量转化核心,通过限时促销信息刺激用户点击购买。其设计需要兼顾:
- 视觉吸引力:通过渐变色和图标营造促销氛围
- 信息密度:在有限空间展示商品图片和价格
- 加载性能:优化图片加载,避免白屏等待
- 容错能力:网络失败时保留用户数据
1.2 UI 设计规范
┌────────────────────────────────────────────────────────────┐
│ 特惠推荐 精选省攻略 │
│ ┌────────────────────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ │图 │ │图 │ │图 │ │
│ │ 🎁 │ │片 │ │片 │ │片 │ │
│ │ 特惠 │ │ │ │ │ │ │ │
│ │ 限时 │ │¥xx │ │¥xx │ │¥xx │ │
│ │ │ └─────┘ └─────┘ └─────┘ │
│ │ 橙红→橙黄渐变 │ │
│ └────────────────────┘ │
└────────────────────────────────────────────────────────────┘
1.3 开发流程
① 定义接口常量 → ② 构建数据模型 → ③ 封装 API
↓
⑦ 完善展示 ← ⑥ 传递数据 ← ⑤ 初始化数据 ← ④ 更新组件
二、数据结构设计
2.1 API 规范
接口地址:GET /hot/preference
响应结构:
{
"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.2 数据模型层次
SpecialOfferResult (特惠推荐结果)
└─ subTypes: List<SubType> (子类型列表)
└─ goodsItems: GoodsItems (商品集合)
└─ items: List<GoodsItem> (商品列表)
└─ [id, name, desc, price, picture, orderNum]
2.3 数据模型代码
/// 商品项
class GoodsItem {
final String id;
final String name;
final String? desc; // 可空字段
final String price;
final String picture;
final 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"] as String? ?? "",
name: json["name"] as String? ?? "",
desc: json["desc"] as String?, // 保持可空
price: json["price"] as String? ?? "0.00",
picture: json["picture"] as String? ?? "",
orderNum: json["orderNum"] as int? ?? 0,
);
}
}
/// 商品集合(分页信息)
class GoodsItems {
final int counts;
final int pageSize;
final int pages;
final int page;
final 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"] as int? ?? 0,
pageSize: json["pageSize"] as int? ?? 0,
pages: json["pages"] as int? ?? 0,
page: json["page"] as int? ?? 0,
items: (json["items"] as List<dynamic>?)
?.map((e) => GoodsItem.fromJSON(e as Map<String, dynamic>))
.toList() ?? [],
);
}
}
/// 子类型
class SubType {
final String id;
final String title;
final GoodsItems goodsItems;
SubType({
required this.id,
required this.title,
required this.goodsItems,
});
factory SubType.fromJSON(Map<String, dynamic> json) {
return SubType(
id: json["id"] as String? ?? "",
title: json["title"] as String? ?? "",
// 空对象兜底
goodsItems: GoodsItems.fromJSON(
json["goodsItems"] as Map<String, dynamic>? ?? {}
),
);
}
}
/// 特惠推荐结果(根对象)
class SpecialOfferResult {
final String id;
final String title;
final 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"] as String? ?? "",
title: json["title"] as String? ?? "",
subTypes: (json["subTypes"] as List<dynamic>?)
?.map((e) => SubType.fromJSON(e as Map<String, dynamic>))
.toList() ?? [],
);
}
}
三、UI 组件实现
3.1 组件结构
class HmSuggestion extends StatefulWidget {
final SpecialOfferResult specialOfferResult;
const HmSuggestion({
super.key,
required this.specialOfferResult,
});
State<HmSuggestion> createState() => _HmSuggestionState();
}
class _HmSuggestionState extends State<HmSuggestion> {
// 获取前3条商品数据
List<GoodsItem> get _displayItems {
if (widget.specialOfferResult.subTypes.isEmpty) {
return [];
}
return widget.specialOfferResult.subTypes.first.goodsItems.items
.take(3)
.toList();
}
Widget build(BuildContext context) {
if (_displayItems.isEmpty) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFF6EE),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildHeader(),
const SizedBox(height: 12),
Row(
children: [
_buildPromoCard(),
const SizedBox(width: 8),
Expanded(child: _buildProductList()),
],
),
],
),
);
}
// 标题栏
Widget _buildHeader() {
return Row(
children: [
const Text(
'特惠推荐',
style: TextStyle(
color: Color(0xFF561814),
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 8),
Text(
widget.specialOfferResult.title.isNotEmpty
? widget.specialOfferResult.title
: '精选省攻略',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF7C3F3A),
),
),
],
);
}
// 左侧促销卡片
Widget _buildPromoCard() {
return Container(
width: 75,
height: 140,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFF7832), Color(0xFFFFB43C)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFFFF8C32).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(2, 4),
),
],
),
child: const 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 _buildProductList() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _displayItems.map((item) => _ProductCard(item: item)).toList(),
);
}
}
3.2 商品卡片组件
class _ProductCard extends StatelessWidget {
final GoodsItem item;
const _ProductCard({required this.item});
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
children: [
// 商品图片
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _OptimizedImage(url: item.picture, height: 140),
),
const SizedBox(height: 10),
// 价格标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: const Color(0xFFF0600C),
),
child: Text(
'¥${item.price}',
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}
3.3 图片优化组件
/// 优化的网络图片组件
/// 支持 HTTP/HTTPS 自动转换、加载进度、错误占位
class _OptimizedImage extends StatelessWidget {
final String url;
final double height;
const _OptimizedImage({
required this.url,
this.height = 140,
});
Widget build(BuildContext context) {
if (url.isEmpty) {
return _ErrorPlaceholder(height: height);
}
// HTTP 转 HTTPS
String secureUrl = url.startsWith('http://')
? url.replaceFirst('http://', 'https://')
: url;
return SizedBox(
width: double.infinity,
height: height,
child: Image.network(
secureUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _LoadingIndicator(
height: height,
progress: loadingProgress,
);
},
errorBuilder: (context, error, stackTrace) {
// HTTPS 失败则尝试原 HTTP URL
if (secureUrl != url) {
return SizedBox(
width: double.infinity,
height: height,
child: Image.network(
url,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _ErrorPlaceholder(height: height),
),
);
}
return _ErrorPlaceholder(height: height);
},
),
);
}
}
/// 加载中指示器
class _LoadingIndicator extends StatelessWidget {
final double height;
final ImageChunkEvent? progress;
const _LoadingIndicator({
required this.height,
this.progress,
});
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey[200],
),
child: Center(
child: CircularProgressIndicator(
value: progress?.expectedTotalBytes != null
? progress!.cumulativeBytesLoaded / progress!.expectedTotalBytes!
: null,
strokeWidth: 2,
),
),
);
}
}
/// 错误占位组件
class _ErrorPlaceholder extends StatelessWidget {
final double height;
const _ErrorPlaceholder({required this.height});
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: height,
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),
),
],
),
),
);
}
}
四、页面集成
4.1 API 封装
class HomeAPI {
/// 获取特惠推荐数据
static Future<SpecialOfferResult> getSpecialOffers() async {
try {
final response = await dioRequest.get(HttpConstants.PRODUCT_LIST);
return SpecialOfferResult.fromJSON(response as Map<String, dynamic>);
} catch (e) {
debugPrint('获取特惠推荐失败: $e');
rethrow;
}
}
}
4.2 首页集成
class _HomeViewState extends State<HomeView> {
SpecialOfferResult _specialOfferResult = SpecialOfferResult(
id: '',
title: '',
subTypes: [],
);
void initState() {
super.initState();
_initData();
}
Future<void> _initData() async {
try {
// 并发请求
await Future.wait([
_getBannerList(),
_getCategoryList(),
_getSpecialOffers(),
]);
} catch (e) {
debugPrint('数据加载失败: $e');
}
}
Future<void> _getSpecialOffers() async {
try {
_specialOfferResult = await HomeAPI.getSpecialOffers();
setState(() {});
} catch (e) {
// 可选择使用默认数据
}
}
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverToBoxAdapter(
child: HmSuggestion(specialOfferResult: _specialOfferResult),
),
],
);
}
}
五、问题排查记录
5.1 问题清单
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 空列表崩溃 | subTypes.isEmpty 时调用 first |
添加空值检查 |
| HTTP 图片失败 | HarmonyOS 禁止明文流量 | 自动转换 HTTPS |
| 白屏等待 | 无加载反馈 | 添加 loadingBuilder |
| desc 字段空指针 | API 返回 null |
使用 String? |
| 配置 Schema 错误 | module.json5 不允许 network 配置 |
使用代码解决方案 |
5.2 空数据保护
// 错误写法
List<GoodsItem> get items => widget.result.subTypes.first.goodsItems.items;
// 正确写法
List<GoodsItem> get displayItems {
if (widget.result.subTypes.isEmpty) return [];
return widget.result.subTypes.first.goodsItems.items.take(3).toList();
}
5.3 HTTP/HTTPS 处理
String _normalizeUrl(String url) {
if (url.isEmpty) return '';
return url.startsWith('http://')
? url.replaceFirst('http://', 'https://')
: url;
}
六、UI 设计规范
| 元素 | 规格 |
|---|---|
| 容器背景 | #FFF6EE 浅米白 |
| 渐变色 | #FF7832 → #FFB43C 橙红到橙黄 |
| 阴影颜色 | #FF8C32 30% 透明度 |
| 价格标签 | #F0600C 橙色背景 |
| 卡片圆角 | 10-12 dp |
| 内边距 | 12 dp |
七、总结
技术要点
- 四层嵌套模型:
GoodsItem → GoodsItems → SubType → SpecialOfferResult - 图片优化:HTTP/HTTPS 自动转换 + 加载进度 + 错误占位
- 空安全处理:可空类型 + 默认值兜底
- 渐变设计:
LinearGradient营造促销氛围
扩展建议
- 添加倒计时组件显示限时信息
- 支持点击商品跳转到详情页
- 实现下拉刷新和加载更多
- 添加骨架屏提升首屏体验
源码地址:https://atomgit.com/lbbxmx111/haromyos_day_four
更多推荐
所有评论(0)