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 扩展建议

  1. 缓存优化:使用 shared_preferences 缓存分类数据
  2. 加载骨架:添加 Shimmer 效果提升感知
  3. 点击交互:支持点击分类跳转到商品列表页
  4. 动画效果:添加分类切换时的动画过渡

源码地址:https://atomgit.com/lbbxmx111/haromyos_day_four

相关资源

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐