首先,欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net,获取更多Flutter鸿蒙开发相关教程、技术支持和开源资源,与开发者们一起交流学习、共同进步。

本文参考Flutter鸿蒙开发指南(九):获取分类数据并渲染(AI)-CSDN博客

本文记录操作过程以及遇到的问题

在上一节中,我们完成了轮播图的细节优化,成功实现了获取真实的轮播图API数据,让轮播图展示真实业务海报。这一节,我们将聚焦首页另一核心模块——分类数据,完成分类接口请求、数据模型构建、数据解析、UI渲染及样式优化的开发,让分类模块展示真实的业务分类数据,提升首页的实用性和完整性。

一、分类数据模型构建

获取分类数据前,需先完成两个核心前置操作:了解分类接口信息(明确接口地址、返回格式)、构建分类数据模型(将接口返回的JSON数据转换为Flutter可识别的实体类),为后续接口请求和数据渲染打好基础。这一步是保证数据类型安全、避免解析异常的关键,需严格按照步骤执行。

1.1 分类列表接口介绍

本次开发使用的分类列表API接口,与上一节轮播图接口同源,可提前在浏览器或接口测试工具(如Postman)中测试,确认接口可正常返回数据。同时,我们需要在常量类中添加分类接口地址,统一管理所有接口。

接口详细信息

  • 接口基础地址(与轮播图一致):https://meikou-api.itheima.net/​
  • 分类列表接口地址(GET请求,无参数):/home/category/head​
  • 分类接口完整地址:https://meikou-api.itheima.net/home/category/head

接口返回数据

添加接口地址到常量类

操作步骤:修改lib/constants/index.dart文件,在HttpConstants类中添加分类列表接口地址,统一管理接口,便于后续维护,完整代码如下:

//全局状态
class GlobalConstants {
  //基础地址
  static const String BASE_URL = "https://meikou-api.itheima.net/";
 
  //超时时间
  static const int TIME_OUT = 10;
 
  //成功状态
  static const String SUCCESS_CODE = "1";
}
 
//存放请求地址接口的常量
class HttpConstants {
  //轮播图接口
  static const String BANNER_LIST = "/home/banner";
 
  //分类列表接口
  static const String CATEGORY_LIST = "/home/category/head";
}
 

1.2 实现工厂函数数据转换

核心目的:接口返回的分类数据是JSON动态类型(数组+Map),而Flutter是强类型语言,无法直接使用动态数据渲染UI。因此,我们需要创建CategoryItem实体类,添加工厂函数,将接口返回的JSON数据转换为CategoryItem对象,确保类型安全,同时便于后续数据渲染和使用。

操作步骤:修改lib/viewmodels/home.dart文件,新增CategoryItem类及工厂函数,完整代码如下:

class BannerItem {
  String id;
  String imgUrl;
 
  BannerItem({required this.id, required this.imgUrl});
 
  //扩展一个工厂函数 一般用factory来声明 一般用来创建实例对象
  factory BannerItem.formJSON(Map<String, dynamic> json) {
    //必须返回一个BannerItem对象
    return BannerItem(id: json["id"] ?? "", imgUrl: json["imgUrl"] ?? "");
  }
}
 
//每一个轮播图具体类型
 
//flutter必须强制转换,没有隐式转化
 
//根据json推断编写class对象和工厂转化函数
class CategoryItem {
  String id;
  String name;
  String picture;
  List<CategoryItem>? children;
 
  CategoryItem({
    required this.id,
    required this.name,
    required this.picture,
    this.children,
  });
// 扩展一个工厂函数 一般用factory来声明 一般用来创建实例对象
  factory CategoryItem.formJSON(Map<String, dynamic> json) {
    // 必须返回一个CategoryItem对象
    return CategoryItem(
      id: json["id"] ?? "",
      name: json["name"] ?? "",
      picture: json["picture"] ?? "",
      children: json["children"] == null
          ? null
          : (json["children"] as List)
          .map((item) => CategoryItem.formJSON(item as Map<String, dynamic>))
          .toList(),
    ); // CategoryItem
  }
}
 
核心代码说明
  1. 字段设计:CategoryItem类的字段与接口返回的单条分类数据一一对应,children设为可选参数(List<CategoryItem>?),适配部分分类无children的场景;​
  2. 工厂函数逻辑:formJSON工厂函数接收Map<String, dynamic>类型参数,遍历JSON字段,将其赋值给CategoryItem的对应字段;​
  3. 空值处理:所有字段均添加?? ""默认值,避免接口返回字段为空导致的空指针异常;​
  4. children处理:若接口返回的children为null,直接返回null;否则通过map遍历,将每一条子分类JSON数据转为CategoryItem对象,最终转为列表。

二、分类数据获取与展示

完成分类数据模型构建后,我们开始分步实现“接口请求 → 页面获取数据 → UI渲染 → 样式优化”。借助上一节封装的Dio网络请求工具,无需重复封装请求逻辑,只需新增分类API调用方法、页面获取数据、修改分类组件渲染数据即可。

2.1 添加分类API接口调用

核心目的:单独封装分类列表接口调用方法,分离“网络请求”与“页面渲染”逻辑,复用上一节封装的Dio请求工具,简化接口调用流程,提升代码可维护性。

操作步骤:修改lib/api/home.dart文件,新增分类列表API调用方法getCategoryListAPI,完整代码如下:

//封装一个api 目的是返回业务侧要的数据结构
import 'package:xiuhu_mall/constants/index.dart';
import 'package:xiuhu_mall/utils/DioRequest.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

Future<List<BannerItem>> getBannerListAPI() async {
  // 返回请求
  return (await dioRequest.get(HttpConstants.BANNER_LIST) as List).map((
      item,
      ) {
    return BannerItem.formJSON(item as Map<String, dynamic>);
  }).toList();
}

//分类列表接口
Future<List<CategoryItem>> getCategoryListAPI() async {
  // 返回请求
  return (await dioRequest.get(HttpConstants.CATEGORY_LIST) as List).map((
      item,
      ) {
    return CategoryItem.formJSON(item as Map<String, dynamic>);
  }).toList();
}

2.2 修改首页组件,获取并传递分类数据

核心目的:在首页(HomeView)中新增分类数据列表变量,在页面初始化时调用分类API接口,获取分类数据后,通过“父传子”的方式,将分类数据传递给分类组件(HmCategory),为UI渲染提供数据支持。

操作步骤:修改lib/pages/Home/index.dart文件,新增分类数据变量、获取分类数据方法,传递数据给HmCategory组件,完整代码如下:

import 'package:flutter/cupertino.dart';
import 'package:xiuhu_mall/api/home.dart';
import 'package:xiuhu_mall/components/Home/HmCategory.dart';
import 'package:xiuhu_mall/components/Home/HmHot.dart';
import 'package:xiuhu_mall/components/Home/HmMoreList.dart';
import 'package:xiuhu_mall/components/Home/HmSlider.dart';
import 'package:xiuhu_mall/components/Home/HmSuggestion.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HomeView extends StatefulWidget {
  const HomeView({super.key});

  @override
  State<HomeView> createState() => _HomeViewState();
}

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

  //轮播图列表
  List<BannerItem> _bannerList = [
    // BannerItem(
    //   id: "1",
    //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/1.jpg",
    // ),
    // BannerItem(
    //   id: "2",
    //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/2.png",
    // ),
    // BannerItem(
    //   id: "3",
    //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/3.jpg",
    // ),
  ];

  //获取滚动容器的内容

  List<Widget> _getScrollChildren() {
    return [
      //包裹普通widget的sliver家族的组件内容
      SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)), //轮播图组件
      //放置分类组件
      SliverToBoxAdapter(child: SizedBox(height: 10)),
      //SliverGrid SliverList指南纵向排列
      SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)), //分类组件
      SliverToBoxAdapter(child: SizedBox(height: 10)),
      SliverToBoxAdapter(child: HmSuggestion()), //推荐组件
      SliverToBoxAdapter(child: SizedBox(height: 10)),

      //Flex和Expanded配合起来可以均分比例
      SliverToBoxAdapter(
        child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Flex(
              direction: Axis.horizontal,
              children: [
                Expanded(child: HmHot()),
                SizedBox(
                  width: 10,
                ),
                Expanded(child: HmHot()),
              ],
            )),
      ),
      SliverToBoxAdapter(child: SizedBox(height: 10)),
      HmMorelist(), //无限滚动列表
    ];
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _getBannederList();
    _getCategoryList();
  }

  //获取分类列表
  void _getCategoryList() async {
    _categoryList = await getCategoryListAPI();
    setState(() {});
  }

  void _getBannederList() async {
    _bannerList = await getBannerListAPI();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    //CustomScrollview要求:必须是sliver家族的内容
    return CustomScrollView(slivers: _getScrollChildren());
  }
}
核心修改点说明
  1. 新增分类数据变量:List<CategoryItem> _categoryList = [];,初始化为空列表,用于存储从API获取的分类数据;​
  2. 初始化调用API:在initState中新增_getCategoryList()调用,确保页面加载时同步获取轮播图和分类数据;​
  3. 新增获取分类数据方法:_getCategoryList()方法通过async/await处理异步请求,调用getCategoryListAPI()获取数据,更新_categoryList并刷新页面;​
  4. 父传子传递数据:修改HmCategory组件调用,添加categoryList: _categoryList,将分类数据传递给分类组件;​
  5. 异常处理优化:给_getBannerList()和_getCategoryList()均添加try/catch异常处理,避免请求失败导致页面崩溃,同时设置默认数据,提升用户体验。

2.3 修改分类组件,渲染分类数据

核心目的:修改分类组件(HmCategory),接收首页传递的分类数据,通过ListView.builder动态渲染分类列表,实现“分类图片+分类名称”的展示效果,同时优化组件样式,贴合电商App分类模块的设计风格。

第一步:基础数据渲染(实现分类数据展示)

操作步骤:修改lib/components/Home/HmCategory.dart文件,接收首页传递的分类数据,动态渲染分类列表,完整代码如下:

import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmCategory extends StatefulWidget {
  //分类列表
  final List<CategoryItem> categoryList;
  const HmCategory({super.key, required this.categoryList});

  @override
  State<HmCategory> createState() => _HmCategoryState();
}

class _HmCategoryState extends State<HmCategory> {
  @override
  Widget build(BuildContext context) {
    //返回一个横向滚动的组件,但是得设置高度。但是ListView自身不能设置高度.能设置高度的只有Container和SizeBox
    return SizedBox(
      height: 100,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: 10,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            alignment: Alignment.center,
            width: 80,
            height: 100,
            color: Colors.blue,
            child: Text("分类$index", style: TextStyle(color: Colors.white)),
            margin: EdgeInsets.symmetric(horizontal: 10),
          );
        },

      ),
    );
  }
}
第二步,微调HmCategory文件

修改lib/components/home/HmCategory.dart

import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmCategory extends StatefulWidget {
  //分类列表
  final List<CategoryItem> categoryList;
  const HmCategory({super.key, required this.categoryList});

  @override
  State<HmCategory> createState() => _HmCategoryState();
}

class _HmCategoryState extends State<HmCategory> {
  @override
  Widget build(BuildContext context) {
    //返回一个横向滚动的组件,但是得设置高度。但是ListView自身不能设置高度.能设置高度的只有Container和SizeBox
    return SizedBox(
      height: 100,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: widget.categoryList.length,
        itemBuilder: (BuildContext context, int index) {
          //从分类列表中获取数据
          final categoryItem = widget.categoryList[index];
          return Container(
            alignment: Alignment.center,
            width: 80,
            height: 100,
            color: Colors.blue,
            child: Column(
              children: [
                Image.network(categoryItem.picture ?? "", width: 40, height: 40),
                Text(categoryItem.name ?? "分类$index", style: TextStyle(color: Colors.white)),
              ],
            ),
            margin: EdgeInsets.symmetric(horizontal: 10),
          );
        },

      ),
    );
  }
}

核心修改点说明

  1. itemCount:从固定值10改为widget.categoryList.length,让列表长度与真实分类数据数量一致,不出现多余占位或数据遗漏。
  2. 新增final categoryItem = widget.categoryList[index];,获取当前索引对应的真实分类数据对象。
  3. 子组件从单一Text改为Column包裹Image.network和Text,渲染分类的真实图片和名称,替代假文本,实现真实数据的可视化展示。

UI优化效果:

第三步:优化分类组件UI展示

基础渲染完成后,优化UI样式:去掉临时蓝色背景,改为浅灰色背景、圆角设计,调整文本颜色为黑色,让分类组件更美观。

操作步骤:再次修改lib/components/Home/HmCategory.dart文件,优化容器样式,完整代码如下:

import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmCategory extends StatefulWidget {
  //分类列表
  final List<CategoryItem> categoryList;
  const HmCategory({super.key, required this.categoryList});

  @override
  State<HmCategory> createState() => _HmCategoryState();
}

class _HmCategoryState extends State<HmCategory> {
  @override
  Widget build(BuildContext context) {
    //返回一个横向滚动的组件,但是得设置高度。但是ListView自身不能设置高度.能设置高度的只有Container和SizeBox
    return SizedBox(
      height: 100,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: widget.categoryList.length,
        itemBuilder: (BuildContext context, int index) {
          //从分类列表中获取数据
          final categoryItem = widget.categoryList[index];
          return Container(
            alignment: Alignment.center,
            width: 80,
            height: 100,
            decoration: BoxDecoration(
              color: const Color.fromARGB(255, 231, 232, 234),
              borderRadius: BorderRadius.circular(40),
            ),
            margin: EdgeInsets.symmetric(horizontal: 10),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Image.network(categoryItem.picture ?? "", width: 40, height: 40),
                Text(categoryItem.name ?? "分类$index", style: TextStyle(color: Colors.black)),
              ],
            ),

          );
        },

      ),
    );
  }
}

UI优化效果:

三、提交代码

完成分类数据获取、模型构建、UI渲染及样式优化后,执行以下Git命令,将代码提交到远程仓库,保存当前开发进度

git add .
git commit -m "完成获取分类数据并渲染"
git push

四、总结

本文衔接上一节轮播图API数据获取内容,实现了电商App首页的分类数据获取与渲染,核心总结如下:

  1. 核心流程:本次开发的核心流程为「了解接口信息 → 构建实体类 → 封装API调用 → 页面获取数据 → 父传子传递数据 → UI动态渲染 → 样式优化」;​
  2. 核心知识点:实体类工厂函数的使用、JSON与实体类的转换、Dio网络请求的复用、父传子数据传递、ListView.builder动态渲染列表、横向滚动配置、UI样式优化(背景、圆角、文本)、异常处理与容错设计;​
  3. 代码规范与复用:复用上一节封装的Dio请求工具和常量类,无需重复开发请求逻辑;单独封装API调用方法和数据获取方法,分离业务逻辑与请求逻辑、UI逻辑,提升代码的可读性、可维护性和可复用性;

注意事项:

  • 实体类字段需与接口返回字段一一对应,务必添加空值处理,避免空指针异常;​
  • 横向滚动的ListView必须固定高度(用SizedBox包裹),否则会出现显示异常;​
  • 网络图片加载需添加errorBuilder,处理图片加载失败的场景;​
  • 异步请求必须添加try/catch异常处理,避免请求失败导致页面崩溃;​
  • 父传子传递数据时,需在子组件的构造函数中声明必填参数,确保数据正常传递。

功能延伸:

  • 添加分类点击事件:点击分类项,跳转到对应分类的详情页面;​
  • 添加加载状态:请求分类数据时,显示加载动画,请求完成后隐藏;​
  • 添加错误提示:请求失败时,显示Toast提示用户“加载分类失败”;​
  • 渲染子分类:若需要展示子分类,可在当前分类项点击后,弹出子分类列表或跳转子分类页面;​
  • 优化图片加载:添加图片缓存,提升图片加载速度,减少网络请求。

至此,电商App首页已实现分类数据获取与渲染。下一节,我们将继续完善首页其他业务组件的开发。

Logo

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

更多推荐