Flutter多个ExpansionTile同步折叠问题求助
问题分析
你的核心问题是所有MapCard共享同一个MapCardViewModel实例——折叠一个ExpansionTile时,ViewModel里的全局状态被重置,导致所有关联组件同步响应。设置Key只能帮助Flutter区分组件实例,但无法解决状态共享的本质问题。
解决方案
下面提供两种可行修复方案,可根据项目架构选择:
方案一:将状态移至MapCard的State类(推荐,适合独立组件场景)
把原本放在ViewModel里的状态,迁移到_MapCardState中,让每个MapCard拥有独立状态:
class MapCard extends StatefulWidget { final String building; final int floor; const MapCard({super.key, required this.building, required this.floor}); @override State<MapCard> createState() => _MapCardState(); } class _MapCardState extends State<MapCard> { // 每个MapCard独立维护自身状态 bool isFloorChosen = false; bool isAreaVisible = false; bool isAreaChosen = false; bool isButtonsVisible = false; int floorNumber = 1; String area = ""; // 模拟数据,实际可从API获取 final List<int> floors = [1,2,3,4]; final List<String> areas = ["Area A", "Area B", "Area C"]; @override void initState() { super.initState(); floorNumber = floors[0]; area = areas[0]; } @override Widget build(BuildContext context) { return Center( child: Card( color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, child: Padding( padding: const EdgeInsets.only(bottom: 12), child: ExpansionTile( key: PageStorageKey(widget.building), onExpansionChanged: (changed) { if (!changed) { setState(() { isAreaVisible = false; isButtonsVisible = false; isFloorChosen = false; floorNumber = floors[0]; area = areas[0]; }); } }, title: Row( children: [ Container( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 8), child: Image.asset( "assets/images/example.png", height: 80, width: 80, ), ), Flexible( child: Container( padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), child: Column( children: [ Text("${widget.building} Apartment \nFloor Count ${widget.floor} ") ], ), ), ) ], ), children: [ const Text("Choose Floor"), Padding( padding: const EdgeInsets.only(right: 24, left: 24), child: DropdownButton( isExpanded: true, value: isFloorChosen == false ? floors[0] : floorNumber, items: floors.map<DropdownMenuItem<int>>((int i) { return DropdownMenuItem<int>( value: i, child: Text(i.toString()), ); }).toList(), onChanged: (int? value) { setState(() { floorNumber = value!; isFloorChosen = true; isAreaVisible = true; }); }, ), ), Visibility( visible: isAreaVisible, child: Column( children: [ const Text("Choose an Area to map"), Padding( padding: const EdgeInsets.only(right: 24, left: 24), child: DropdownButton( isExpanded: true, value: isAreaChosen == false ? areas[0] : area, items: areas.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), onChanged: (String? value) { setState(() { area = value!; isAreaChosen = true; isButtonsVisible = true; }); }, ), ), ], ), ), Visibility( visible: isButtonsVisible, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( onPressed: () { showDialog( context: context, builder: (BuildContext context) { return CustomDialog( title: "Mapping Status", content: "This area hasn't been mapped yet", page: Container(), buttonColor: MainColors().mainBlue, ); }, ); }, child: const Text("Show Area Map"), ), ElevatedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const MappedPage(), ), ); }, style: ElevatedButton.styleFrom(backgroundColor: MainColors().mainBlue), child: const Text("Map The Area", style: TextStyle(color: Colors.white)), ) ], ), ) ], ), ), ), ), ); } }
方案二:修改ViewModel支持多实例状态隔离(适合需全局共享部分数据的场景)
若必须使用ViewModel,可让它通过Map结构为每个building维护独立状态:
修改后的ViewModel示例
class MapCardViewModel extends ChangeNotifier { // 以building名称为key,存储每个卡片的独立状态 final Map<String, CardState> _cardStates = {}; // 全局共享的列表数据 final List<int> floors = [1,2,3,4]; final List<String> areas = ["Area A", "Area B", "Area C"]; // 获取指定building的状态,不存在则初始化 CardState getCardState(String building) { if (!_cardStates.containsKey(building)) { _cardStates[building] = CardState( floorNumber: floors[0], area: areas[0], isFloorChosen: false, isAreaVisible: false, isAreaChosen: false, isButtonsVisible: false, ); } return _cardStates[building]!; } // 重置指定building的状态 void resetCardState(String building) { if (_cardStates.containsKey(building)) { _cardStates[building] = CardState( floorNumber: floors[0], area: areas[0], isFloorChosen: false, isAreaVisible: false, isAreaChosen: false, isButtonsVisible: false, ); notifyListeners(); } } // 更新楼层选择 void setFloorNumber(String building, int value) { final state = getCardState(building); state.floorNumber = value; state.isFloorChosen = true; state.isAreaVisible = true; notifyListeners(); } // 更新区域选择 void setArea(String building, String value) { final state = getCardState(building); state.area = value; state.isAreaChosen = true; state.isButtonsVisible = true; notifyListeners(); } } // 单独定义卡片状态类 class CardState { int floorNumber; String area; bool isFloorChosen; bool isAreaVisible; bool isAreaChosen; bool isButtonsVisible; CardState({ required this.floorNumber, required this.area, required this.isFloorChosen, required this.isAreaVisible, required this.isAreaChosen, required this.isButtonsVisible, }); }
修改后的MapCard使用方式
class _MapCardState extends State<MapCard> { @override Widget build(BuildContext context) { return Center( child: Consumer<MapCardViewModel>( builder: (context, vm, child) { final cardState = vm.getCardState(widget.building); return Card( color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, child: Padding( padding: const EdgeInsets.only(bottom: 12), child: ExpansionTile( key: PageStorageKey(widget.building), onExpansionChanged: (changed) { if (!changed) vm.resetCardState(widget.building); }, title: Row( children: [ Container( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 8), child: Image.asset( "assets/images/example.png", height: 80, width: 80, ), ), Flexible( child: Container( padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), child: Column( children: [ Text("${widget.building} Apartment \nFloor Count ${widget.floor} ") ], ), ), ) ], ), children: [ const Text("Choose Floor"), Padding( padding: const EdgeInsets.only(right: 24, left: 24), child: DropdownButton( isExpanded: true, value: cardState.isFloorChosen == false ? vm.floors[0] : cardState.floorNumber, items: vm.floors.map<DropdownMenuItem<int>>((int i) { return DropdownMenuItem<int>( value: i, child: Text(i.toString()), ); }).toList(), onChanged: (int? value) => vm.setFloorNumber(widget.building, value!), ), ), Visibility( visible: cardState.isAreaVisible, child: Column( children: [ const Text("Choose an Area to map"), Padding( padding: const EdgeInsets.only(right: 24, left: 24), child: DropdownButton( isExpanded: true, value: cardState.isAreaChosen == false ? vm.areas[0] : cardState.area, items: vm.areas.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), onChanged: (String? value) => vm.setArea(widget.building, value!), ), ), ], ), ), Visibility( visible: cardState.isButtonsVisible, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( onPressed: () { showDialog( context: context, builder: (BuildContext context) { return CustomDialog( title: "Mapping Status", content: "This area hasn't been mapped yet", page: Container(), buttonColor: MainColors().mainBlue, ); }, ); }, child: const Text("Show Area Map"), ), ElevatedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const MappedPage(), ), ); }, style: ElevatedButton.styleFrom(backgroundColor: MainColors().mainBlue), child: const Text("Map The Area", style: TextStyle(color: Colors.white)), ) ], ), ) ], ), ), ), ); }, ), ); } }
关键说明
- 方案一:每个
MapCard完全独立,状态互不干扰,代码更简洁,适合组件无需全局共享状态的场景。 - 方案二:通过ViewModel的Map结构隔离状态,适合需要在其他组件中访问卡片状态的场景。
内容的提问来源于stack exchange,提问作者vivus
相关产品推荐
相关产品推荐

