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

Flutter:如何处理带TabBar的可滚动SliverAppBar及单标签分页

实现带TabBar的可滚动SliverAppBar及分页功能(Flutter)

核心思路

用NestedScrollView协调可滚动的SliverAppBar和TabBarView的滚动联动,每个标签页单独维护自己的ScrollController来监听滚动触发分页请求,同时通过innerScrollPositionKeyBuilder保留切换标签后的滚动位置。


步骤1:基础布局搭建

先构建包含可折叠SliverAppBar和TabBar的外层结构,TabBarView的每个子项对应一个带分页功能的列表组件。

class TabbedScrollablePage extends StatefulWidget {
  const TabbedScrollablePage({super.key});

  @override
  State<TabbedScrollablePage> createState() => _TabbedScrollablePageState();
}

class _TabbedScrollablePageState extends State<TabbedScrollablePage> with SingleTickerProviderStateMixin {
  late final TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NestedScrollView(
        // 关键:给每个标签页的滚动位置绑定唯一Key,切换时保留滚动状态
        innerScrollPositionKeyBuilder: (index) => PageStorageKey('tab_$index'),
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return [
            SliverAppBar(
              title: const Text('可滚动标题栏'),
              expandedHeight: 200,
              floating: true,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.network(
                  'https://picsum.photos/800/400',
                  fit: BoxFit.cover,
                ),
              ),
              bottom: TabBar(
                controller: _tabController,
                tabs: const [
                  Tab(text: '标签1'),
                  Tab(text: '标签2'),
                  Tab(text: '标签3'),
                  Tab(text: '标签4'),
                ],
              ),
            ),
          ];
        },
        body: TabBarView(
          controller: _tabController,
          children: const [
            PaginatedList(tabIndex: 0),
            PaginatedList(tabIndex: 1),
            PaginatedList(tabIndex: 2),
            PaginatedList(tabIndex: 3),
          ],
        ),
      ),
    );
  }
}

步骤2:实现带分页的列表组件

每个标签页的列表单独维护ScrollController、分页状态(页码、加载状态、数据列表),监听滚动事件触发API请求。

class PaginatedList extends StatefulWidget {
  final int tabIndex;
  const PaginatedList({super.key, required this.tabIndex});

  @override
  State<PaginatedList> createState() => _PaginatedListState();
}

class _PaginatedListState extends State<PaginatedList> {
  late final ScrollController _scrollController;
  List<String> _data = [];
  int _currentPage = 1;
  bool _isLoading = false;
  final int _pageSize = 10;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    // 监听滚动到底部
    _scrollController.addListener(_onScroll);
    // 初始化加载第一页
    _fetchData();
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  // 模拟API请求,根据tabIndex返回不同数据
  Future<void> _fetchData() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);

    // 模拟网络延迟
    await Future.delayed(const Duration(seconds: 1));
    // 根据标签索引生成模拟数据
    final newData = List.generate(_pageSize, (index) => '标签${widget.tabIndex} - 条目${(_currentPage-1)*_pageSize + index + 1}');
    
    setState(() {
      _data.addAll(newData);
      _currentPage++;
      _isLoading = false;
    });
  }

  // 滚动监听逻辑
  void _onScroll() {
    // 判断是否滚动到接近底部(预留200px触发加载)
    if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !_isLoading) {
      _fetchData();
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _data.length + (_isLoading ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == _data.length) {
          // 加载中占位符
          return const Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Center(child: CircularProgressIndicator()),
          );
        }
        return ListTile(title: Text(_data[index]));
      },
    );
  }
}

关键细节说明

  • 滚动联动与状态保留:NestedScrollView的innerScrollPositionKeyBuilder通过PageStorageKey为每个标签页的滚动位置绑定唯一标识,切换标签时不会丢失之前的滚动位置。
  • 分页触发逻辑:每个列表的ScrollController独立监听滚动事件,通过判断position.pixels与maxScrollExtent的差值,提前触发加载更多,避免用户等待。
  • 重复请求拦截:通过_isLoading状态标记,防止滚动时多次触发API请求。
  • TabController管理:外层页面维护TabController,确保TabBar和TabBarView的选中状态同步。

内容的提问来源于stack exchange,提问作者Suman Poudyal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 09:20:31