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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:52:04