如何让Flutter中嵌套的NestedScrollView正常工作?
问题现象
在Flutter中嵌套使用NestedScrollView(内层包含ListView)时,会抛出栈溢出错误:
═════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building PrimaryScrollController(no controller):
Stack Overflow
若给内层ListView绑定PrimaryScrollController,错误会指向ListView:
═════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building ListView(scrollDirection: vertical, _NestedScrollController#81c19(inner, one client, offset 0.0), ClampingScrollPhysics, dependencies: [MediaQuery]):
Stack OverflowThe relevant error-causing widget was
ListView
复现代码
import 'package:flutter/material.dart'; void main() async { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const NestedScrollView1(); } } class NestedScrollView1 extends StatelessWidget { const NestedScrollView1({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: NestedScrollView( physics: const ClampingScrollPhysics(), headerSliverBuilder: (_, __) => [ SliverToBoxAdapter( child: Container( color: Colors.blue, height: 100, ), ) ], body: NestedScrollView2(), ), ); } } class NestedScrollView2 extends StatelessWidget { final ScrollController scrollController = ScrollController(); NestedScrollView2({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return NestedScrollView( controller: PrimaryScrollController.of(context), physics: const ClampingScrollPhysics(), headerSliverBuilder: (ctx, __) => [ SliverToBoxAdapter( child: Container( color: Colors.red, height: 100, ), ), ], body: const ListOfItems(), ); } } class ListOfItems extends StatelessWidget { const ListOfItems({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListView( physics: const ClampingScrollPhysics(), // controller: PrimaryScrollController.of(context), children: [ Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), ], ); } }
场景说明
实际项目中存在嵌套TabBarView结构:外层页面包含TabBarView,其中一个子项又有独立的TabBarView,希望通过NestedScrollView的headerSliverBuilder来承载标签栏,需要无需调整UI结构或编写复杂自定义滚动逻辑的解决办法。
解决办法
核心思路:避免NestedScrollView嵌套
NestedScrollView的设计初衷是处理单一层级的嵌套滚动(比如header+可滚动body),嵌套使用会导致ScrollController的循环引用,进而触发栈溢出。以下是两种低成本的替代方案:
方案1:内层用CustomScrollView替代NestedScrollView
把内层的NestedScrollView换成CustomScrollView,既保留header+列表的结构,又不会和外层NestedScrollView产生控制器冲突:
class NestedScrollView2 extends StatelessWidget { NestedScrollView2({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return CustomScrollView( physics: const ClampingScrollPhysics(), slivers: [ // 原内层NestedScrollView的header SliverToBoxAdapter( child: Container( color: Colors.red, height: 100, ), ), // 将ListView转为SliverList SliverList( delegate: SliverChildListDelegate([ Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), ]), ), ], ); } }
方案2:给内层NestedScrollView使用独立控制器
如果必须保留内层NestedScrollView,不要使用父级的PrimaryScrollController,而是创建独立的ScrollController,同时禁止内层ListView的滚动物理效果,让它跟随外层滚动:
class NestedScrollView2 extends StatelessWidget { // 创建独立的控制器,不依赖父级PrimaryScrollController final ScrollController _innerController = ScrollController(); NestedScrollView2({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return NestedScrollView( controller: _innerController, physics: const ClampingScrollPhysics(), headerSliverBuilder: (ctx, __) => [ SliverToBoxAdapter( child: Container( color: Colors.red, height: 100, ), ), ], body: const ListOfItems(), ); } } class ListOfItems extends StatelessWidget { const ListOfItems({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListView( // 禁止ListView自身滚动,跟随外层NestedScrollView physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, children: [ Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), Container(color: Colors.green, height: 200), Container(color: Colors.yellow, height: 200), ], ); } }
针对嵌套TabBarView的优化建议
如果是外层TabBarView嵌套内层TabBarView的场景,建议将外层的NestedScrollView的body直接设置为外层TabBarView,每个外层Tab的内容如果需要header+列表,直接用CustomScrollView实现,这样整个页面的滚动逻辑统一,不会出现控制器冲突。
内容的提问来源于stack exchange,提问作者Linet

