Flutter评论BottomSheet setState不即时更新UI问题解决
问题描述
- 复用的
CommentBottomSheet组件通过showModalBottomSheet展示评论弹窗,调用setState更新isLoading和commentsData时,数据已更新但弹窗UI不会即时刷新,需点击屏幕才显示变化。
问题根源
弹窗是通过showModalBottomSheet创建的独立路由,其内部UI依赖CommentBottomSheetState的状态,但父组件的setState只会刷新自身的build方法(返回空Scaffold),无法触发弹窗内部的UI重建。弹窗的Widget树与父组件Widget树分离,父组件状态变化不会自动传递到弹窗。
修复方案
将弹窗核心内容提取为独立的StatefulWidget,让弹窗内容直接监听状态变化,确保父组件状态更新时弹窗能同步刷新:
- 创建
_CommentSheetContent组件,接收状态变量(isLoading、commentsData、isReplying)和回调方法(状态更新、提交操作等)。 - 父组件
setState更新状态后,弹窗组件会收到新参数,自动触发UI刷新。
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shimmer/shimmer.dart'; import 'package:iconsax/iconsax.dart'; import 'package:lottie/lottie.dart'; import '../../services/api_service.dart'; import '../../utils/shared_pref.dart'; class CommentBottomSheet extends StatefulWidget { final int postIndex; const CommentBottomSheet({super.key, required this.postIndex}); @override CommentBottomSheetState createState() => CommentBottomSheetState(); } class CommentBottomSheetState extends State<CommentBottomSheet> { bool isReplying = false; bool isLoading = true; List<dynamic> commentsData = []; final ApiService _apiService = ApiService(); final ScrollController _scrollController = ScrollController(); final TextEditingController _replyController = TextEditingController(); final TextEditingController _commentController = TextEditingController(); @override void initState(){ super.initState(); _fetchComments(); WidgetsBinding.instance.addPostFrameCallback((_){ _showFullScreenBottomSheet(); }); } @override void dispose(){ _scrollController.dispose(); _replyController.dispose(); _commentController.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return const Scaffold(body: SizedBox.shrink()); } void _showFullScreenBottomSheet(){ showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (BuildContext context) { return _CommentSheetContent( isLoading: isLoading, commentsData: commentsData, isReplying: isReplying, scrollController: _scrollController, replyController: _replyController, commentController: _commentController, onToggleReply: () { setState(() { isReplying = !isReplying; if (!isReplying) _replyController.clear(); }); }, onSubmitComment: () { print("Comment submitted: ${_commentController.text}"); _commentController.clear(); // 可添加提交评论后重新获取数据的逻辑 }, onSubmitReply: () { print("Reply submitted: ${_replyController.text}"); setState(() { isReplying = false; _replyController.clear(); }); // 可添加提交回复后重新获取数据的逻辑 }, onReplyTap: () { setState(() { isReplying = true; }); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); }, ); }, ).whenComplete(() { Navigator.of(context).pop(); }); } Future<void> _fetchComments() async { var prefs = await SharedPreferences.getInstance(); String? selfUserID = prefs.getString(CustomSharedConstants.userID); String? userSessionToken = prefs.getString(CustomSharedConstants.userSessionToken); try{ var response = await _apiService.get(context, 'get-post-comments/$selfUserID/${widget.postIndex}', userSessionToken ?? ""); int responseStatusCode = response['statusCode']; if(responseStatusCode == 200){ List<dynamic> responseBody = response['body']["postData"]; if(mounted){ setState((){ isLoading = false; commentsData = responseBody; }); } } else { if (mounted) { setState(() { isLoading = false; commentsData = []; }); } } } catch (e) { if (mounted) { setState(() { isLoading = false; commentsData = []; }); } } } } class _CommentSheetContent extends StatefulWidget { final bool isLoading; final List<dynamic> commentsData; final bool isReplying; final ScrollController scrollController; final TextEditingController replyController; final TextEditingController commentController; final VoidCallback onToggleReply; final VoidCallback onSubmitComment; final VoidCallback onSubmitReply; final VoidCallback onReplyTap; const _CommentSheetContent({ required this.isLoading, required this.commentsData, required this.isReplying, required this.scrollController, required this.replyController, required this.commentController, required this.onToggleReply, required this.onSubmitComment, required this.onSubmitReply, required this.onReplyTap, }); @override __CommentSheetContentState createState() => __CommentSheetContentState(); } class __CommentSheetContentState extends State<_CommentSheetContent> { @override Widget build(BuildContext context) { return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Container( height: MediaQuery.of(context).size.height * 0.93, padding: const EdgeInsets.only(top: 12.0), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, spreadRadius: 5), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container(width: 50, height: 5, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(5))), ), const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Comments', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey[800])), IconButton( icon: const Icon(Icons.close, color: Colors.grey[600]), onPressed: (){ Navigator.pop(context); }, ), ], ), ), Expanded( child: widget.isLoading ? ListView.builder( itemCount: 5, itemBuilder: (context, index) { return Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar(backgroundColor: Colors.grey[300]!, radius: 20), const SizedBox(width: 12.0), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(color: Colors.grey[300]!, height: 20, width: 100), const SizedBox(height: 4.0), Container(color: Colors.grey[300]!, height: 14, width: double.infinity), const SizedBox(height: 8.0), Container(color: Colors.grey[300]!, height: 14, width: 80), ], ), ), ], ), ), ); }, ) : widget.commentsData.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Lottie.asset('assets/lottie/no_chats_cards.json', width: 150, height: 150), const SizedBox(height: 20), const Text('No comment found', style: TextStyle(fontSize: 16, color: Colors.grey[800])), const SizedBox(height: 10), const Text('Be the first to comment on this post', style: TextStyle(fontSize: 14, color: Colors.grey[600])), ], ), ) : SingleChildScrollView( controller: widget.scrollController, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: widget.commentsData .map((comment) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( backgroundImage: NetworkImage(comment['CommentUserData']['UserProfilePicture']), radius: 20, ), const SizedBox(width: 12.0), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('${comment['CommentUserData']['UserFirstName']} ${comment['CommentUserData']['UserLastName']}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), Text(comment['UpdatedAtTimeAgo'], style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), const SizedBox(height: 4.0), Text(comment['CommentContent'], style: TextStyle(fontSize: 14, color: Colors.grey[800])), const SizedBox(height: 8.0), Row( children: [ const LikeButton(), const SizedBox(width: 16.0), GestureDetector( onTap: widget.onReplyTap, child: Row( children: [ const Icon(Icons.reply, color: Colors.grey[600], size: 18), const SizedBox(width: 4.0), const Text('Reply', style: TextStyle(fontSize: 14, color: Colors.grey[600])), ], ), ), ], ), ], ), ), ], ), )) .toList(), ), ), ), Padding( padding: EdgeInsets.only(left: 16.0, right: 16.0, bottom: MediaQuery.of(context).viewInsets.bottom + 16.0), child: Row( children: [ Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.grey[300]!)), child: TextField( controller: widget.isReplying ? widget.replyController : widget.commentController, decoration: InputDecoration( hintText: widget.isReplying ? 'Write a reply...' : 'Write a comment...', border: InputBorder.none, hintStyle: TextStyle(color: Colors.grey[600]), suffixIcon: widget.isReplying ? IconButton( icon: const Icon(Icons.close, color: Colors.grey[600]), onPressed: widget.onToggleReply, ) : null, ), onTap: () { WidgetsBinding.instance.addPostFrameCallback((_) { widget.scrollController.jumpTo(widget.scrollController.position.maxScrollExtent); }); }, ), ), ), const SizedBox(width: 8.0), GestureDetector( onTap: widget.isReplying ? widget.onSubmitReply : widget.onSubmitComment, child: Container( padding: const EdgeInsets.all(12.0), decoration: const BoxDecoration(color: Color(0xFF8E2DE2), shape: BoxShape.circle), child: const Icon(Icons.send, color: Colors.white), ), ), ], ), ), ], ), ), ); } } class LikeButton extends StatefulWidget { const LikeButton({super.key}); @override LikeButtonState createState() => LikeButtonState(); } class LikeButtonState extends State<LikeButton> { bool isLiked = false; @override Widget build(BuildContext context) { return IconButton( icon: Icon(isLiked ? Iconsax.heart5 : Iconsax.heart, color: isLiked ? Colors.red : Colors.grey), onPressed: (){ setState((){ isLiked = !isLiked; }); }, ); } }
关键修改点
- 将弹窗核心UI提取为
_CommentSheetContent独立组件,通过props接收状态和回调,实现状态与UI的绑定。 - 父组件状态更新时,弹窗组件会自动接收新props并触发UI重建,解决了原有的刷新延迟问题。
- 所有交互逻辑通过回调方法传递,确保状态变更能同步到弹窗UI。
内容的提问来源于stack exchange,提问作者yogeshdinodia
相关产品推荐
相关产品推荐

