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

Flutter Web中NestedScrollView切换Tab时ScrollController多绑定错误求助

Flutter Web滚动错误解决方案

问题说明

开发Flutter Web应用时,使用带Tabs的SliverAppBar和NestedScrollView实现滚动功能,出现如下错误:

The Scrollbar requires a single ScrollPosition in order to be painted.
When the scrollbar is interactive, the associated ScrollController must only have one ScrollPosition
attached. The provided ScrollController must be unique to one ScrollView widget.

触发场景:滚动某个Tab后切换到另一Tab,在Tab未完全切换完成时切回原Tab。

已尝试无效方案:给CustomScrollView添加ScrollController、设置primary: false(后者会导致SliverAppBar无法随内容滚动,且错误仍存在)。

复现代码

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _selectedIndex = 0;

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  static const TextStyle optionStyle =
      TextStyle(fontSize: 30, fontWeight: FontWeight.bold);

  static const List<Widget> _widgetOptions = <Widget>[
    Text(
      'Index 0: Home',
      style: optionStyle,
    ),
    Text(
      'Index 1: Business',
      style: optionStyle,
    ),
    Text(
      'Index 2: School',
      style: optionStyle,
    ),
  ];

  SliverAppBar showSliverAppBar(BuildContext context, String screenTitle) {
    final screenSize = MediaQuery.of(context).size;

    return SliverAppBar(
      elevation: 15,
      backgroundColor: Theme.of(context).colorScheme.primary,
      floating: true,
      pinned: true,
      snap: false,
      centerTitle: true,
      title: Center(
        child: Text(
          screenTitle,
          textAlign: TextAlign.center,
        ),
      ),
      actions: [
        Padding(
          padding: EdgeInsets.only(
            right: screenSize.width * 0.01,
          ),
          child: IconButton(
            onPressed: () {},
            icon: const Icon(
              Icons.person,
            ),
          ),
        ),
      ],
      bottom: const TabBar(
        tabs: [
          Tab(
            icon: Icon(Icons.home),
            text: 'Home',
          ),
          Tab(
            icon: Icon(Icons.calendar_month_outlined),
            text: 'Calendar',
          ),
          Tab(
            icon: Icon(Icons.question_mark),
            text: 'About Us',
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const Drawer(
        child: Column(
          children: <Widget>[
            DrawerHeader(
              curve: SawTooth(12),
              child: Text('I am Drawer'),
            ),
          ],
        ),
      ),
      body: DefaultTabController(
        length: 3,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              showSliverAppBar(context, 'Nested Scroll View Error'),
            ];
          },
          body: TabBarView(
            children: [
              CustomScrollView(
                slivers: [
                  SliverList(
                    delegate: SliverChildListDelegate(
                      [
                        Container(
                          height: 600,
                          color: Theme.of(context).colorScheme.secondary,
                          child: const Center(
                            child: Text(
                              'Calendar Tab',
                              style: TextStyle(fontSize: 40),
                            ),
                          ),
                        ),
                        Container(
                          height: 1500,
                          color: Colors.green,
                          child: Center(
                            child: _widgetOptions.elementAt(_selectedIndex),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              CustomScrollView(
                slivers: [
                  SliverList(
                    delegate: SliverChildListDelegate(
                      [
                        Container(
                          height: 600,
                          color: Colors.blue[200],
                          child: const Center(
                            child: Text('Calendar Tab',
                                style: TextStyle(fontSize: 40)),
                          ),
                        ),
                        Container(
                          height: 1200,
                          color: Colors.pink,
                          child: Center(
                            child: _widgetOptions.elementAt(_selectedIndex),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              CustomScrollView(
                slivers: [
                  SliverList(
                    delegate: SliverChildListDelegate(
                      [
                        Container(
                          height: 600,
                          color: Colors.blue[200],
                          child: const Center(
                            child: Text('About Us Tab',
                                style: TextStyle(fontSize: 40)),
                          ),
                        ),
                        Container(
                          height: 1200,
                          color: Colors.pink,
                          child: Center(
                            child: _widgetOptions.elementAt(_selectedIndex),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: 'Business',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.school),
            label: 'School',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }
}

解决方案

问题根源是Tab切换过程中,未完全隐藏的旧Tab的ScrollView仍持有ScrollPosition,与新Tab的ScrollView共享默认ScrollController导致冲突。以下是可行的解决方法:

方法1:为每个CustomScrollView分配独立的ScrollController

在State类中初始化三个独立的控制器:

final _scrollControllers = [
  ScrollController(),
  ScrollController(),
  ScrollController(),
];

修改NestedScrollView和每个CustomScrollView的配置:

NestedScrollView(
  controller: _scrollControllers[_selectedIndex],
  headerSliverBuilder: ...,
  body: TabBarView(
    children: [
      CustomScrollView(
        controller: _scrollControllers[0],
        primary: false,
        slivers: ...,
      ),
      // 另外两个CustomScrollView同理绑定对应控制器并设置primary: false
    ],
  ),
)

每个Tab的ScrollView使用独立控制器,避免共享导致的ScrollPosition冲突。

方法2:用AutomaticKeepAliveClientMixin保持Tab状态

创建封装CustomScrollView的状态组件,确保Tab切换时不销毁旧ScrollView:

class TabContent extends StatefulWidget {
  final Widget child;
  const TabContent({Key? key, required this.child}) : super(key: key);

  @override
  State<TabContent> createState() => _TabContentState();
}

class _TabContentState extends State<TabContent> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }
}

在TabBarView中用TabContent包裹每个CustomScrollView:

TabBarView(
  children: [
    TabContent(
      child: CustomScrollView(slivers: ...),
    ),
    // 另外两个Tab同理包裹
  ],
)

旧Tab的ScrollView会被保留,不会重复创建导致控制器绑定多个ScrollPosition。

方法3:禁用交互式滚动条(临时方案)

若不需要滚动条交互功能,可全局或单个禁用:

  • 全局禁用:
MaterialApp(
  scrollBehavior: const MaterialScrollBehavior().copyWith(scrollbars: false),
  // 其他配置
)
  • 单个ScrollView禁用:
CustomScrollView(
  scrollBehavior: const MaterialScrollBehavior().copyWith(scrollbars: false),
  slivers: ...,
)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 18:05:42