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
相关产品推荐
相关产品推荐

