【HarmonyOS】DAY9:Flutter 鸿蒙电商开发:分类数据获取与渲染完整指南
本文详细介绍在 Flutter/HarmonyOS 项目中实现电商分类数据获取与渲染的完整流程。从 API 接口定义、数据模型构建、网络请求封装到 UI 组件渲染,提供一套规范化的开发方法论,并记录常见问题的解决方案。
·
Flutter 鸿蒙电商开发:分类数据获取与渲染完整指南
摘要:本文详细介绍在 Flutter/HarmonyOS 项目中实现电商分类数据获取与渲染的完整流程。从 API 接口定义、数据模型构建、网络请求封装到 UI 组件渲染,提供一套规范化的开发方法论,并记录常见问题的解决方案。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、功能概述
1.1 业务场景
在电商应用中,分类导航是用户发现商品的核心入口。典型的分类模块需要满足以下需求:
- 横向滚动展示:支持多个分类项左右滑动浏览
- 图文结合:显示分类图标和名称
- 数据驱动:通过 API 动态获取分类数据
- 嵌套结构:支持二级分类展示
1.2 技术栈
| 技术组件 | 用途 |
|---|---|
ListView.builder |
横向滚动列表 |
| Dio | HTTP 网络请求 |
| factory 模式 | JSON 数据解析 |
| 空安全 (Null Safety) | 类型安全保障 |
1.3 开发流程规范
┌─────────────────────────────────────────────────────────┐
│ 标准化七步开发流程 │
├─────────────────────────────────────────────────────────┤
│ ① 定义常量 │ 接口地址、超时时间、业务状态码 │
├─────────────────────────────────────────────────────────┤
│ ② 封装网络 │ 基础地址、拦截器、统一响应处理 │
├─────────────────────────────────────────────────────────┤
│ ③ 解构响应 │ HTTP 状态码与业务状态码分离处理 │
├─────────────────────────────────────────────────────────┤
│ ④ 数据模型 │ 工厂函数将动态 JSON 转为强类型对象 │
├─────────────────────────────────────────────────────────┤
│ ⑤ API 封装 │ 业务接口函数,返回类型化数据 │
├─────────────────────────────────────────────────────────┤
│ ⑥ 页面集成 │ 组件接收数据参数,更新 UI │
├─────────────────────────────────────────────────────────┤
│ ⑦ 状态驱动 │ 初始化请求,setState 触发渲染 │
└─────────────────────────────────────────────────────────┘
二、接口规范与数据结构
2.1 API 定义
接口地址:GET /home/category/head
基础地址:https://meikou-api.itheima.net
请求示例:
curl -X GET "https://meikou-api.itheima.net/home/category/head"
响应结构:
{
"code": "1",
"msg": "操作成功",
"result": [
{
"id": "1181622001",
"name": "气质女装",
"picture": "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meikou/c1/qznz.png",
"children": [
{
"id": "1191110001",
"name": "半裙",
"picture": "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meikou/c2/qznz_bq.png"
}
]
}
]
}
2.2 数据结构分析
result (数组)
└─ CategoryItem (对象)
├─ id: String // 分类 ID
├─ name: String // 分类名称
├─ picture: String // 分类图片 URL
└─ children: Array? // 子分类(可空)
└─ CategoryItem // 递归结构
三、代码实现详解
3.1 常量定义
文件路径:lib/constants/index.dart
/// HTTP 接口常量定义
class HttpConstants {
// 基础配置
static const String BASE_URL = "https://meikou-api.itheima.net";
static const Duration TIMEOUT = Duration(seconds: 10);
// 业务接口
static const String BANNER_LIST = "/home/banner";
static const String CATEGORY_LIST = "/home/category/head";
// 业务状态码
static const String SUCCESS_CODE = "1";
}
3.2 数据模型
文件路径:lib/viewmodels/home.dart
/// 分类数据模型
/// 支持递归嵌套结构(二级分类)
class CategoryItem {
final String id;
final String name;
final String picture;
final List<CategoryItem>? children;
CategoryItem({
required this.id,
required this.name,
required this.picture,
this.children,
});
/// 工厂构造函数:从 JSON 创建实例
///
/// 使用空安全操作符 `??` 提供默认值,防止空指针异常
factory CategoryItem.fromJSON(Map<String, dynamic> json) {
return CategoryItem(
id: json["id"] as String? ?? "",
name: json["name"] as String? ?? "",
picture: json["picture"] as String? ?? "",
// 递归处理子分类
children: json["children"] == null
? null
: (json["children"] as List<dynamic>)
.map((item) => CategoryItem.fromJSON(item as Map<String, dynamic>))
.toList(),
);
}
/// 转换为 JSON(可选,用于本地缓存)
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'picture': picture,
'children': children?.map((e) => e.toJson()).toList(),
};
}
}
3.3 网络请求封装
文件路径:lib/api/home.dart
import '../constants/index.dart';
import '../viewmodels/home.dart';
import 'dio.dart';
/// 首页相关 API
class HomeAPI {
/// 获取分类列表
///
/// 返回分类项列表,失败时抛出异常
static Future<List<CategoryItem>> getCategoryList() async {
try {
// 发起 GET 请求
final response = await dioRequest.get(HttpConstants.CATEGORY_LIST);
// 类型转换:List<dynamic> → List<CategoryItem>
final resultList = response as List<dynamic>;
return resultList
.map((item) => CategoryItem.fromJSON(item as Map<String, dynamic>))
.toList();
} catch (e) {
// 可在此添加日志记录
rethrow;
}
}
}
3.4 UI 组件实现
文件路径:lib/components/Home/HmCategory.dart
import 'package:flutter/material.dart';
import '../../viewmodels/home.dart';
/// 分类横向滚动组件
class HmCategory extends StatefulWidget {
final List<CategoryItem> categoryList;
const HmCategory({
super.key,
required this.categoryList,
});
State<HmCategory> createState() => _HmCategoryState();
}
class _HmCategoryState extends State<HmCategory> {
Widget build(BuildContext context) {
// 空数据处理
if (widget.categoryList.isEmpty) {
return const SizedBox.shrink();
}
return SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: widget.categoryList.length,
itemBuilder: (context, index) {
final item = widget.categoryList[index];
return _CategoryCard(item: item);
},
),
);
}
}
/// 单个分类卡片
class _CategoryCard extends StatelessWidget {
final CategoryItem item;
const _CategoryCard({required this.item});
Widget build(BuildContext context) {
return Container(
width: 80,
height: 100,
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: const Color(0xFFE7E8EA),
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 分类图标
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
item.picture,
width: 48,
height: 48,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 48,
height: 48,
color: Colors.grey[300],
child: const Icon(Icons.image_not_supported, size: 24),
);
},
),
),
const SizedBox(height: 8),
// 分类名称
Text(
item.name,
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
3.5 页面集成
文件路径:lib/pages/home/index.dart
class _HomeViewState extends State<HomeView> {
// 状态变量
List<CategoryItem> _categoryList = [];
List<BannerItem> _bannerList = [];
bool _isLoading = false;
void initState() {
super.initState();
_initData();
}
/// 初始化页面数据
Future<void> _initData() async {
setState(() => _isLoading = true);
try {
// 并发请求多个接口
final results = await Future.wait([
_getBannerList(),
_getCategoryList(),
]);
setState(() => _isLoading = false);
} catch (e) {
setState(() => _isLoading = false);
_showErrorToast('数据加载失败,请重试');
}
}
/// 获取分类数据
Future<void> _getCategoryList() async {
try {
_categoryList = await HomeAPI.getCategoryList();
setState(() {}); // 触发 UI 更新
} catch (e) {
debugPrint('获取分类失败: $e');
// 可选择使用默认数据或显示错误状态
}
}
/// 获取轮播图数据
Future<void> _getBannerList() async {
try {
_bannerList = await HomeAPI.getBannerList();
setState(() {});
} catch (e) {
debugPrint('获取轮播图失败: $e');
}
}
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)),
// 其他组件...
],
);
}
}
四、常见问题与解决方案
4.1 空安全类型转换错误
错误信息:
type 'Null' is not a subtype of type 'String'
原因分析:
API 返回的字段可能为 null,直接强制转换导致类型错误。
解决方案:
// ❌ 错误写法
id: json["id"] as String
// ✅ 正确写法
id: json["id"] as String? ?? ""
4.2 嵌套数组解析失败
错误信息:
type 'List<dynamic>' is not a subtype of type 'List<CategoryItem>'
解决方案:
// 使用 map 逐项转换
children: (json["children"] as List<dynamic>)
.map((item) => CategoryItem.fromJSON(item as Map<String, dynamic>))
.toList()
4.3 UI 不更新问题
原因分析:
异步数据获取后未调用 setState。
解决方案:
void _getCategoryList() async {
try {
_categoryList = await HomeAPI.getCategoryList();
setState(() {}); // 必须调用以触发重建
} catch (e) {
debugPrint('Error: $e');
}
}
4.4 网络图片加载失败
解决方案:
Image.network(
item.picture,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 48,
height: 48,
color: Colors.grey[200],
child: const Icon(Icons.broken_image),
);
},
)
五、UI 渲染效果
5.1 组件结构
┌──────────────────────────────────────────────────────────┐
│ 分类横向滚动区 │
├──────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ ┌───┐ │ │ ┌───┐ │ │ ┌───┐ │ │ ┌───┐ │ │
│ │ │图标│ │ │ │图标│ │ │ │图标│ │ │ │图标│ │ │
│ │ └───┘ │ │ └───┘ │ │ └───┘ │ │ └───┘ │ │
│ │ 气质女装 │ │ 潮流男装 │ │ 数码3C │ │ 美妆个护 │ →│
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────┘
← 滚动查看更多分类 →
5.2 视觉规范
| 属性 | 值 | 说明 |
|---|---|---|
| 卡片尺寸 | 80×100 dp | 圆角 20 |
| 图标尺寸 | 48×48 dp | 圆角 12 |
| 文字大小 | 12 sp | 最大 1 行,省略截断 |
| 卡片间距 | 12 dp | 水平外边距 |
| 背景色 | #E7E8EA | 浅灰 |
六、总结
6.1 技术要点
| 要点 | 说明 |
|---|---|
| 工厂模式 | factory 关键字实现 JSON 解析 |
| 空安全 | ?? 操作符提供默认值 |
| 递归结构 | children 字段支持嵌套分类 |
| 错误处理 | try-catch + errorBuilder |
| 状态管理 | setState 驱动 UI 更新 |
6.2 扩展建议
- 缓存优化:使用
shared_preferences缓存分类数据 - 加载骨架:添加
Shimmer效果提升感知 - 点击交互:支持点击分类跳转到商品列表页
- 动画效果:添加分类切换时的动画过渡
源码地址:https://atomgit.com/lbbxmx111/haromyos_day_four
相关资源:
更多推荐




所有评论(0)