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

Flutter实现优惠券卡片单次仅展开一张的技术咨询

优惠券卡片单展开实现方案

问题描述

我开发了一套优惠券卡片功能,当前每张卡片点击“View Details”按钮可独立展开或收起,现需求为同一时间仅允许展开一张卡片。以下是原始实现代码:

coupon_page.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../Utils/GlobalColor/global_color.dart';
import '../Utils/GlobalWidgetPages/app_bar_center_text.dart';
import 'Components/coupon_card.dart';


class CouponPage extends StatefulWidget {
  const CouponPage({Key? key}) : super(key: key);

  @override
  State<CouponPage> createState() => _CouponPageState();
}

class _CouponPageState extends State<CouponPage> {
  List input = [
    {
      "Id": 1,
      "OfferTitle": "50% off on 2nd Pizza",
    },
    {
      "Id": 3,
      "OfferTitle": "FREE CHOCOLAVA CAKE",
    },
    {
      "Id": 6,
      "OfferTitle": "60% OFF",
    },
    {
      "Id": 7,
      "OfferTitle": "STEP DOWN BOGO",
    },
    {
      "Id": 8,
      "OfferTitle": "FREE GARLIC BREAD STICKS",
    },
    {
      "Id": 9,
      "OfferTitle": "FLAT 25% OFF",
    }
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBarWithCenterText(
          text: "Coupon & Offers",
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        backgroundColor: MyColor.bgLightColor,
        body: ListView.builder(
            physics: BouncingScrollPhysics(),
            padding: const EdgeInsets.only(top: 21.0, bottom: 20),
            shrinkWrap: true,
            itemCount: input.length,
            itemBuilder: (_, index) {
              return Padding(
                padding: EdgeInsets.only(
                  top: index == 0 ? 0 : 12,
                ),
                child: CouponCard(data: input[index]),
              );
            }));
  }
}

coupon_card.dart

import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import '../../Utils/GlobalColor/global_color.dart';
import '../../Utils/GlobalTextStyles/global_text_styles.dart';

class CouponCard extends StatefulWidget {
  final data;

  const CouponCard({Key? key, this.data}) : super(key: key);

  @override
  State<CouponCard> createState() => _CouponCardState();
}

class _CouponCardState extends State<CouponCard> {
  bool isViewDetailsOpen = false;

  @override
  Widget build(BuildContext context) {
    return Container(
        padding: EdgeInsets.fromLTRB(0, 16, 0, 0),
        margin: EdgeInsets.fromLTRB(20, 0, 20, 0),
        width: double.infinity,
        decoration: BoxDecoration(
          color: MyColor.whiteColor,
          borderRadius: BorderRadius.circular(8),
          boxShadow: [
            BoxShadow(
              color: MyColor.greyColor.withOpacity(0.2),
              blurRadius: 7.0,
              spreadRadius: 3,
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 12),
              child: Text(
                "${widget.data["Id"]}",
                style: textStyleWith14500(MyColor.blackColor),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 14),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  InkWell(
                    onTap: () {
                      setState(() {
                        isViewDetailsOpen = !isViewDetailsOpen;
                      });
                    },
                    child: Row(
                      children: [
                        Text(
                          "View Details",
                          style: textStyleWith12400(MyColor.darkGreyColor),
                        ),
                        SizedBox(
                          width: 6,
                        ),
                        Icon(
                          !isViewDetailsOpen
                              ? Icons.arrow_drop_down
                              : Icons.arrow_drop_up,
                          color: MyColor.darkGreyColor,
                          size: 20,
                        )
                      ],
                    ),
                  ),
                ],
              ),
            ),
            isViewDetailsOpen
                ? Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(top: 7.0, bottom: 14),
                        child: DottedBorder(
                          padding: EdgeInsets.zero,
                          color: MyColor.greyColor,
                          dashPattern: [9, 9],
                          child: Container(),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                          left: 16.0,
                          right: 16,
                        ),
                        child: Text(
                          "Code ",
                          style: textStyleWith12600(MyColor.blackColor),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                          left: 16.0,
                          right: 16,
                          top: 5,
                          bottom: 5,
                        ),
                        child: Text(
                          "${widget.data["OfferTitle"]}",
                          style: textStyleWith12600(MyColor.blackColor),
                        ),
                      ),
                    ],
                  )
                : Container(),
            InkWell(
              onTap: () {},
              child: Container(
                height: 40,
                decoration: BoxDecoration(
                    color: MyColor.greyColor2,
                    borderRadius: BorderRadius.only(
                        bottomRight: Radius.circular(8),
                        bottomLeft: Radius.circular(8))),
                child: Center(
                  child: Text(
                    "TAP TO APPLY",
                    style: textStyleWith12500(MyColor.darkGreyColor),
                  ),
                ),
              ),
            )
          ],
        ));
  }
}

解决方案

核心思路是将卡片的展开状态**统一托管在父组件CouponPage**中,替代原有的子卡片独立维护状态的模式,具体修改如下:

1. 修改父组件CouponPage

  • 添加expandedCouponId变量,记录当前展开的卡片ID(null表示全部收起)
  • 为CouponCard传入展开状态判断和点击回调方法,点击时更新expandedCouponId

修改后的coupon_page.dart代码:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../Utils/GlobalColor/global_color.dart';
import '../Utils/GlobalWidgetPages/app_bar_center_text.dart';
import 'Components/coupon_card.dart';


class CouponPage extends StatefulWidget {
  const CouponPage({Key? key}) : super(key: key);

  @override
  State<CouponPage> createState() => _CouponPageState();
}

class _CouponPageState extends State<CouponPage> {
  List input = [
    {
      "Id": 1,
      "OfferTitle": "50% off on 2nd Pizza",
    },
    {
      "Id": 3,
      "OfferTitle": "FREE CHOCOLAVA CAKE",
    },
    {
      "Id": 6,
      "OfferTitle": "60% OFF",
    },
    {
      "Id": 7,
      "OfferTitle": "STEP DOWN BOGO",
    },
    {
      "Id": 8,
      "OfferTitle": "FREE GARLIC BREAD STICKS",
    },
    {
      "Id": 9,
      "OfferTitle": "FLAT 25% OFF",
    }
  ];

  // 记录当前展开的卡片ID,null表示所有卡片都收起
  int? expandedCouponId;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBarWithCenterText(
          text: "Coupon & Offers",
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        backgroundColor: MyColor.bgLightColor,
        body: ListView.builder(
            physics: BouncingScrollPhysics(),
            padding: const EdgeInsets.only(top: 21.0, bottom: 20),
            shrinkWrap: true,
            itemCount: input.length,
            itemBuilder: (_, index) {
              final coupon = input[index];
              return Padding(
                padding: EdgeInsets.only(
                  top: index == 0 ? 0 : 12,
                ),
                child: CouponCard(
                  data: coupon,
                  isExpanded: expandedCouponId == coupon["Id"],
                  onToggle: () {
                    setState(() {
                      // 点击逻辑:当前展开则收起,否则展开当前并关闭其他
                      expandedCouponId = expandedCouponId == coupon["Id"] 
                          ? null 
                          : coupon["Id"];
                    });
                  },
                ),
              );
            }));
  }
}

2. 修改子组件CouponCard

  • 将CouponCard改为无状态组件StatelessWidget
  • 接收父组件传入的isExpanded状态和onToggle回调
  • 移除自身的状态变量,直接使用父组件传递的状态控制展开/收起

修改后的coupon_card.dart代码:

import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import '../../Utils/GlobalColor/global_color.dart';
import '../../Utils/GlobalTextStyles/global_text_styles.dart';

class CouponCard extends StatelessWidget {
  final data;
  final bool isExpanded;
  final VoidCallback onToggle;

  const CouponCard({
    Key? key,
    this.data,
    required this.isExpanded,
    required this.onToggle,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
        padding: EdgeInsets.fromLTRB(0, 16, 0, 0),
        margin: EdgeInsets.fromLTRB(20, 0, 20, 0),
        width: double.infinity,
        decoration: BoxDecoration(
          color: MyColor.whiteColor,
          borderRadius: BorderRadius.circular(8),
          boxShadow: [
            BoxShadow(
              color: MyColor.greyColor.withOpacity(0.2),
              blurRadius: 7.0,
              spreadRadius: 3,
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 12),
              child: Text(
                "${data["Id"]}",
                style: textStyleWith14500(MyColor.blackColor),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 14),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  InkWell(
                    onTap: onToggle,
                    child: Row(
                      children: [
                        Text(
                          "View Details",
                          style: textStyleWith12400(MyColor.darkGreyColor),
                        ),
                        SizedBox(
                          width: 6,
                        ),
                        Icon(
                          !isExpanded
                              ? Icons.arrow_drop_down
                              : Icons.arrow_drop_up,
                          color: MyColor.darkGreyColor,
                          size: 20,
                        )
                      ],
                    ),
                  ),
                ],
              ),
            ),
            isExpanded
                ? Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(top: 7.0, bottom: 14),
                        child: DottedBorder(
                          padding: EdgeInsets.zero,
                          color: MyColor.greyColor,
                          dashPattern: [9, 9],
                          child: Container(),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                          left: 16.0,
                          right: 16,
                        ),
                        child: Text(
                          "Code ",
                          style: textStyleWith12600(MyColor.blackColor),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                          left: 16.0,
                          right: 16,
                          top: 5,
                          bottom: 5,
                        ),
                        child: Text(
                          "${data["OfferTitle"]}",
                          style: textStyleWith12600(MyColor.blackColor),
                        ),
                      ),
                    ],
                  )
                : Container(),
            InkWell(
              onTap: () {},
              child: Container(
                height: 40,
                decoration: BoxDecoration(
                    color: MyColor.greyColor2,
                    borderRadius: BorderRadius.only(
                        bottomRight: Radius.circular(8),
                        bottomLeft: Radius.circular(8))),
                child: Center(
                  child: Text(
                    "TAP TO APPLY",
                    style: textStyleWith12500(MyColor.darkGreyColor),
                  ),
                ),
              ),
            )
          ],
        ));
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 16:39:54