You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter遍历动态键API响应报错,需实现店铺商品列表视图

解决Flutter中API响应解析与ListView展示的问题

我来帮你一步步搞定这个API解析和UI展示的问题,咱们先把核心问题拆解,再逐个击破:

一、修正Model类的序列化问题

首先解决你提到的两个核心错误:

  1. 字段名映射错误:把item字段错误映射成product,我们需要在Model里明确指定JSON字段名;
  2. id类型不兼容:响应中Item的id既有int(比如35)又有String(比如"jk"),所以我们可以把id类型设为dynamic,或者统一转成String类型来兼容两种情况。

这里推荐使用json_serializable包来做序列化,既规范又不容易出错,先看修正后的Model类:

import 'package:json_annotation/json_annotation.dart';

part 'cart_models.g.dart';

// 顶层响应Model
@JsonSerializable()
class CartResponse {
  final bool success;
  final Map<String, List<CartItem>> data;

  CartResponse({required this.success, required this.data});

  factory CartResponse.fromJson(Map<String, dynamic> json) =>
      _$CartResponseFromJson(json);
}

// 购物车条目Model
@JsonSerializable()
class CartItem {
  final int id;
  final int cart_id;
  // 修正字段映射:指定JSON字段为"item"
  @JsonKey(name: "item")
  final Item product;

  CartItem({required this.id, required this.cart_id, required this.product});

  factory CartItem.fromJson(Map<String, dynamic> json) =>
      _$CartItemFromJson(json);
}

// 商品Model
@JsonSerializable()
class Item {
  // 用dynamic兼容int和String类型的id,或者用String并在fromJson里转换
  final dynamic id;
  final String name;

  Item({required this.id, required this.name});

  factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);

  // 如果想统一转成String,可以自定义fromJson逻辑:
  // factory Item.fromJson(Map<String, dynamic> json) {
  //   return Item(
  //     id: json['id'].toString(),
  //     name: json['name'],
  //   );
  // }
}

记得运行flutter pub run build_runner build生成对应的序列化代码。

二、解析API响应

拿到API返回的字符串后,先转成Map,再用我们的Model类解析:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<CartResponse> fetchCartData() async {
  final response = await http.get(Uri.parse("你的API接口地址"));
  if (response.statusCode == 200) {
    final Map<String, dynamic> jsonData = jsonDecode(response.body);
    return CartResponse.fromJson(jsonData);
  } else {
    throw Exception("加载购物车数据失败");
  }
}

三、实现按店铺分组的ListView

接下来我们要实现先展示店铺名称,再在每个店铺下展示对应商品的UI。这里提供两种常用方案:

方案1:使用ExpansionTile(可折叠店铺列表)

适合需要折叠/展开店铺商品的场景:

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

  @override
  State<CartScreen> createState() => _CartScreenState();
}

class _CartScreenState extends State<CartScreen> {
  late Future<CartResponse> _cartFuture;

  @override
  void initState() {
    super.initState();
    _cartFuture = fetchCartData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("我的购物车")),
      body: FutureBuilder<CartResponse>(
        future: _cartFuture,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final cartData = snapshot.data!;
            return ListView.builder(
              itemCount: cartData.data.length,
              itemBuilder: (context, index) {
                final storeName = cartData.data.keys.elementAt(index);
                final items = cartData.data[storeName]!;
                return ExpansionTile(
                  title: Text(storeName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  children: [
                    ListView.builder(
                      shrinkWrap: true,
                      physics: const NeverScrollableScrollPhysics(),
                      itemCount: items.length,
                      itemBuilder: (context, itemIndex) {
                        final product = items[itemIndex].product;
                        return ListTile(
                          leading: Text(product.id.toString()),
                          title: Text(product.name),
                          subtitle: Text("购物车条目ID: ${items[itemIndex].id}"),
                        );
                      },
                    ),
                  ],
                );
              },
            );
          } else if (snapshot.hasError) {
            return Center(child: Text("${snapshot.error}"));
          }
          return const Center(child: CircularProgressIndicator());
        },
      ),
    );
  }
}

方案2:直接平铺展示(不可折叠)

如果不需要折叠功能,可以直接用Column嵌套展示:

// 在FutureBuilder的hasData分支替换为以下代码:
return ListView.builder(
  itemCount: cartData.data.length,
  itemBuilder: (context, index) {
    final storeName = cartData.data.keys.elementAt(index);
    final items = cartData.data[storeName]!;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(storeName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        ),
        ...items.map((item) {
          final product = item.product;
          return ListTile(
            leading: Text(product.id.toString()),
            title: Text(product.name),
            subtitle: Text("购物车条目ID: ${item.id}"),
          );
        }).toList(),
      ],
    );
  },
);

这样就能完美解决你的问题:既修正了API解析的两个错误,又实现了按店铺分组展示商品的ListView。

内容的提问来源于stack exchange,提问作者vellai durai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:54:26