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

Flutter评论BottomSheet setState不即时更新UI问题解决

问题描述
  • 复用的CommentBottomSheet组件通过showModalBottomSheet展示评论弹窗,调用setState更新isLoading和commentsData时,数据已更新但弹窗UI不会即时刷新,需点击屏幕才显示变化。
问题根源

弹窗是通过showModalBottomSheet创建的独立路由,其内部UI依赖CommentBottomSheetState的状态,但父组件的setState只会刷新自身的build方法(返回空Scaffold),无法触发弹窗内部的UI重建。弹窗的Widget树与父组件Widget树分离,父组件状态变化不会自动传递到弹窗。

修复方案

将弹窗核心内容提取为独立的StatefulWidget,让弹窗内容直接监听状态变化,确保父组件状态更新时弹窗能同步刷新:

  1. 创建_CommentSheetContent组件,接收状态变量(isLoading、commentsData、isReplying)和回调方法(状态更新、提交操作等)。
  2. 父组件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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 14:50:54