Flutter BottomSheet分页失效:数据加载后UI未更新
问题:BottomSheet分页加载后UI不更新
在BottomSheet中实现分页加载数据,ScrollController能触发loadMore函数,但BottomSheet的UI无法同步更新。尝试用StatefulBuilder包裹BottomSheet后仍无效果。
原代码
// return 10 string List<String> getStrings() { return [ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]; } class SecondScreen extends StatefulWidget { SecondScreen({Key? key}) : super(key: key); @override State<SecondScreen> createState() => _SecondScreenState(); } class _SecondScreenState extends State<SecondScreen> { final _controller = ScrollController(); // initial list of data List<String> data = [ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]; bool isLoading = false; // A flag to prevent duplicate loading @override void initState() { super.initState(); _controller.addListener(loadMore); } loadMore() { // Only load more if we're at the bottom of the list and not already loading if (_controller.position.maxScrollExtent == _controller.position.pixels && !isLoading) { setState(() { isLoading = true; // Set flag to prevent multiple loads }); // Simulate a network or slow operation (e.g., fetching more data) Future.delayed(const Duration(seconds: 2), () { setState(() { // Add new items to the list data.addAll(getStrings()); isLoading = false; // Reset flag after data is added }); }); } } @override void dispose() { _controller.removeListener(loadMore); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Second Screen')), body: Center( child: ElevatedButton( onPressed: () { showModalBottomSheet( context: context, builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState /*You can rename this!*/) { return SizedBox( height: 300, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), controller: _controller, itemCount: data.length + (isLoading ? 1 : 0), // Show loading indicator if needed itemBuilder: (context, index) { // Display a loading indicator if it's the last item if (index == data.length && isLoading) { setState(() {}); return const Center( child: CircularProgressIndicator()); } return ListTile( title: Text('${data[index]} ${index + 1}'), ); }, ), ); }, ); }, ); }, child: const Text('Show Bottom Sheet'), ), ), ); } }
问题原因
- 原代码中
loadMore调用的是页面State的setState,但showModalBottomSheet是独立于页面的弹窗,页面的setState无法触发BottomSheet的UI重建。 StatefulBuilder的setState仅作用于BottomSheet内部,但原代码中并没有用它来更新数据或状态,反而在itemBuilder里做了空的setState(() {})调用,毫无意义。- 页面级的
ScrollController被复用,多次打开BottomSheet可能导致状态混乱。
解决方案
方案1:将分页逻辑封装到独立的BottomSheet组件
把BottomSheet的内容做成独立的StatefulWidget,将数据、ScrollController、加载逻辑都封装在内部,这样内部的setState能直接触发BottomSheet的UI更新。
// return 10 string List<String> getStrings() { return [ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]; } class SecondScreen extends StatefulWidget { SecondScreen({Key? key}) : super(key: key); @override State<SecondScreen> createState() => _SecondScreenState(); } class _SecondScreenState extends State<SecondScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Second Screen')), body: Center( child: ElevatedButton( onPressed: () { showModalBottomSheet( context: context, builder: (context) => const _BottomSheetContent(), ); }, child: const Text('Show Bottom Sheet'), ), ), ); } } // 独立封装BottomSheet内容及分页逻辑 class _BottomSheetContent extends StatefulWidget { const _BottomSheetContent(); @override State<_BottomSheetContent> createState() => _BottomSheetContentState(); } class _BottomSheetContentState extends State<_BottomSheetContent> { final _controller = ScrollController(); List<String> data = [ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]; bool isLoading = false; @override void initState() { super.initState(); _controller.addListener(loadMore); } void loadMore() { if (_controller.position.maxScrollExtent == _controller.position.pixels && !isLoading) { setState(() => isLoading = true); Future.delayed(const Duration(seconds: 2), () { setState(() { data.addAll(getStrings()); isLoading = false; }); }); } } @override void dispose() { _controller.removeListener(loadMore); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( height: 300, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), controller: _controller, itemCount: data.length + (isLoading ? 1 : 0), itemBuilder: (context, index) { if (index == data.length && isLoading) { return const Center(child: CircularProgressIndicator()); } return ListTile( title: Text('${data[index]} ${index + 1}'), ); }, ), ); } }
优势:
- 逻辑内聚,BottomSheet的状态和UI更新完全由自身控制,避免和页面状态耦合。
- 每次打开BottomSheet都会创建新的
ScrollController,避免复用导致的状态冲突。
方案2:用ValueNotifier+ValueListenableBuilder实现数据监听
如果需要和页面共享数据,可以用ValueNotifier存储数据和加载状态,BottomSheet通过ValueListenableBuilder监听变化,自动更新UI。
// return 10 string List<String> getStrings() { return [ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]; } class SecondScreen extends StatefulWidget { SecondScreen({Key? key}) : super(key: key); @override State<SecondScreen> createState() => _SecondScreenState(); } class _SecondScreenState extends State<SecondScreen> { final _controller = ScrollController(); final ValueNotifier<List<String>> _dataNotifier = ValueNotifier([ "string1", "string2", "string3", "string4", "string5", "string6", "string7", "string8", "string9", "string10" ]); final ValueNotifier<bool> _isLoadingNotifier = ValueNotifier(false); @override void initState() { super.initState(); _controller.addListener(loadMore); } void loadMore() { if (_controller.position.maxScrollExtent == _controller.position.pixels && !_isLoadingNotifier.value) { _isLoadingNotifier.value = true; Future.delayed(const Duration(seconds: 2), () { // 注意:必须创建新的List触发ValueNotifier更新 _dataNotifier.value = [..._dataNotifier.value, ...getStrings()]; _isLoadingNotifier.value = false; }); } } @override void dispose() { _controller.removeListener(loadMore); _controller.dispose(); _dataNotifier.dispose(); _isLoadingNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Second Screen')), body: Center( child: ElevatedButton( onPressed: () { showModalBottomSheet( context: context, builder: (context) { return SizedBox( height: 300, child: ValueListenableBuilder( valueListenable: _dataNotifier, builder: (context, data, child) { return ValueListenableBuilder( valueListenable: _isLoadingNotifier, builder: (context, isLoading, child) { return ListView.builder( physics: const AlwaysScrollableScrollPhysics(), controller: _controller, itemCount: data.length + (isLoading ? 1 : 0), itemBuilder: (context, index) { if (index == data.length && isLoading) { return const Center(child: CircularProgressIndicator()); } return ListTile( title: Text('${data[index]} ${index + 1}'), ); }, ); }, ); }, ), ); }, ); }, child: const Text('Show Bottom Sheet'), ), ), ); } }
优势:
- 数据和状态通过
ValueNotifier管理,页面和BottomSheet可以共享数据。 - 无需依赖
setState,通过监听自动更新UI,逻辑更清晰。
内容的提问来源于stack exchange,提问作者vishal
相关产品推荐
相关产品推荐

