Flutter Alert内DropdownButton点击选项后状态不更新问题排查
问题描述
我在开发Flutter应用时,需要在Alert对话框中使用DropdownButton。当前组件是StatefulWidget,点击DropdownItem后已经调用了setState,但按钮显示始终没有变化,无法同步更新选中的选项。
问题原因
Dialog通过showDialog创建后,会生成独立的Widget树,拥有自己的上下文。外层MealItem的setState只能触发自身组件的重建,无法影响Dialog内部的Widget状态,因此DropdownButton的显示不会跟随选中值更新。
解决方案
使用StatefulBuilder包裹Dialog的内容部分,它可以在Dialog内部提供局部的状态更新能力,同时能感知外层状态的变化,确保DropdownButton的显示与选中值同步。
修正后的代码
核心修改是用StatefulBuilder包裹Dialog主体,确保状态变化能触发Dialog内部重建:
// ignore_for_file: use_key_in_widget_constructors, avoid_print, unrelated_type_equality_checks import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:apetit_project/models/app_user.dart'; import 'package:apetit_project/models/reservation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../models/meal.dart'; import '../utils/app_routes.dart'; class MealItem extends StatefulWidget { final Meal meal; MealItem( this.meal, ); @override State<MealItem> createState() => _MealItemState(); } class _MealItemState extends State<MealItem> { final List<Reservation> _items = []; String dropDownValue = '11:30'; List<Reservation> get items => [..._items]; final AppUser? appUser = const AppUser( id: 'PedroNovoTeste', email: 'email', company: 'GTFoods', ); void addReservation(Reservation reservation) { final future = http.post( Uri.parse( 'http://172.16.30.120:8080/ords/apiteste/integrafoods/cadastra-empresa'), headers: <String, String>{ 'Content-Type': 'application/json; charset=UTF-8', }, body: jsonEncode<String, String>({ "cnpj": reservation.user, "razao": reservation.company, "fantasia": reservation.opcao, }), ); future.then((response) { _items.add(reservation); }); } void _newReservation() { final newReservation = Reservation( user: appUser!.id, date: DateTime.now().add(const Duration(days: 2)), opcao: widget.meal.id, company: appUser!.company, resTime: dropDownValue); addReservation(newReservation); print(newReservation.user); print(newReservation.date); print(newReservation.opcao); print(newReservation.company); print(newReservation.isReserved); print(newReservation.resTime); } void _selectMeal(BuildContext context) { Navigator.of(context) .pushNamed( AppRoutes.MEAL_DETAIL, arguments: widget.meal, ) .then((result) { if (result == null) { print('Sem resultado!'); } else { print('O nome da refeição é $result.'); } }); } List<DropdownMenuItem<String>> get dropdownItems { List<DropdownMenuItem<String>> menuItems = [ const DropdownMenuItem(value: "11:30", child: Text("11:30")), const DropdownMenuItem(value: "12:00", child: Text("12:00")), const DropdownMenuItem(value: "12:30", child: Text("12:30")), const DropdownMenuItem(value: "13:00", child: Text("13:00")), const DropdownMenuItem(value: "13:30", child: Text("13:30")), ]; return menuItems; } @override Widget build(BuildContext context) { final reservation = Provider.of<Reservation>(context); final date = DateTime.now().add(const Duration(days: 2)); String formattedDate = DateFormat('dd/MM/yyyy').format(date); return InkWell( onTap: (widget.meal.descricao == 'Reservar') ? () { showDialog( context: context, builder: (context) { // 用StatefulBuilder包裹Dialog内容 return StatefulBuilder( builder: (dialogContext, setStateDialog) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30)), child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), gradient: LinearGradient( colors: [ Colors.white.withOpacity(0.2), Colors.grey.withOpacity(0.2), Colors.black.withOpacity(0.2), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ SingleChildScrollView( child: ListBody( children: <Widget>[ ListTile( title: Padding( padding: const EdgeInsets.only(top: 15), child: Image.asset( 'assets/images/logo.png', fit: BoxFit.contain, height: 80, ), ), ), Column( children: [ Text( (reservation.isReserved == false) ? 'Confirmar reserva para o dia $formattedDate no restaurante administrativo da empresa GTFoods(Matriz) para o horário' : 'Você já possui uma reserva para o dia $formattedDate no restaurante administrativo da empresa GTFoods(Matriz), deseja cancelar sua reserva?', textAlign: TextAlign.center, style: const TextStyle( fontSize: 15, fontFamily: 'Raleway', fontWeight: FontWeight.bold), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ DropdownButton<String>( value: dropDownValue, items: <String>[ '11:30', '12:00', '12:30', '13:00', '13:30', ].map<DropdownMenuItem<String>>( (String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), onChanged: (String? newValue) { // 更新外层状态,同时触发Dialog内部重建 setState(() { dropDownValue = newValue!; }); }, ), const Text('?') ], ), ], ), const SizedBox( height: 10, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (reservation.isReserved == false) ElevatedButton( child: const Text('Sim'), onPressed: () { _newReservation(); reservation.toggleReserved(); Navigator.pop(context); }, ), if (reservation.isReserved == true) ElevatedButton( child: const Text('Sim'), onPressed: () { print(reservation.isReserved); reservation.toggleReserved(); Navigator.pop(context); }, ), ElevatedButton( child: const Text('Não'), onPressed: () => Navigator.pop(context), ), ], ), ListTile( title: Image.asset( 'assets/images/logo_gttech.png', fit: BoxFit.contain, height: 40, ), ), ], ), ), ], ), ), ); }, ); }, ); } : () => _selectMeal(context), child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), elevation: 0, margin: const EdgeInsets.only(right: 0, left: 0), child: Column( children: [ Stack( children: [ ClipRRect( child: Image.asset( widget.meal.imageUrl, height: (widget.meal.descricao == 'Reservar') ? 50 : 73, width: double.infinity, fit: BoxFit.cover, ), ), Container( height: (widget.meal.descricao == 'Reservar') ? 0 : 73, decoration: BoxDecoration( gradient: LinearGradient( begin: FractionalOffset.centerRight, end: FractionalOffset.centerLeft, colors: [ Colors.grey.withOpacity(0.0), Colors.black.withOpacity(0.9), ], stops: const [0.0, 1.0], ), ), ), Positioned( bottom: (widget.meal.descricao == 'Reservar') ? 7 : (widget.meal.descricao == 'Feijoada') ? 15 : (widget.meal.descricao == 'X Salada') ? 15 : 0, left: (widget.meal.descricao == 'Reservar') ? 95 : 10, child: Container( width: (widget.meal.descricao.length > 22) ? 300 : (widget.meal.descricao.length > 20) ? 210 : (widget.meal.descricao == 'Reservar') ? 210 : (widget.meal.descricao == 'Bife Americano') ? 150 : (widget.meal.descricao == 'Feijão Tropeiro') ? 150 : (widget.meal.descricao.length > 14) ? 200 : 150, padding: const EdgeInsets.symmetric( vertical: 5, horizontal: 15, ), child: Text( widget.meal.descricao, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 24, color: Colors.white, ), softWrap: true, overflow: TextOverflow.fade, ), ), ), ], ), ], ), ), ); } }
额外说明
- 因为
dropDownValue需要在Dialog外的_newReservation方法中使用,所以状态仍保留在_MealItemState中,通过外层setState更新后,StatefulBuilder会感知到值的变化并重建Dialog内部组件。 - 如果Dialog需要管理独立的内部状态,可以将变量放在
StatefulBuilder的builder函数内部,但此场景下无需这么做。
内容的提问来源于stack exchange,提问作者pedro.curti
相关产品推荐
相关产品推荐

