When trying to use the NestedScrollView with a ListView inside a different NestedScrollView Flutter throws a stack overflow error:
════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building PrimaryScrollController(no controller):
Stack Overflow
Here's a minimal-ish code where it happens:
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),
],
);
}
}
if you uncomment the controller line in ListView
- it throws a stack overflow like this:
════════ 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 Overflow
The relevant error-causing widget was
ListView
Thing is on my project I have a page with a TabBarView and one of its sections has a TabBarView of it's own, and I wanted to use the NestedScrollView's to hold the tabs inside headerSliverBuilder's. Is there any way to go around this, without telling designer to reconsider the page UI or building complex custom scroll logic?
Edit: for clarity, adding a draw.io screenshot of the layout I'm trying to achieve (cannot put images right into the posts yet, ugh).
(Forgot to post an answer, better late than never I hope)
I have managed to achieve what I needed in a hack-ish solution from my colleague of just using CustomScrollView with the nested TabBar and ListView inside of SliverFillRemaining:
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 MaterialApp(
home: OuterTabView(),
);
}
}
class OuterTabView extends StatefulWidget {
const OuterTabView({Key? key}) : super(key: key);
@override
State<OuterTabView> createState() => _OuterTabViewState();
}
class _OuterTabViewState extends State<OuterTabView> with TickerProviderStateMixin {
late TabController _tabControllerOut;
@override
void initState() {
super.initState();
_tabControllerOut = TabController(length: 3, vsync: this);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (_, __) {
return <Widget>[
SliverToBoxAdapter(
child: TabBar(
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
SizedBox(height: 40),
],
controller: _tabControllerOut,
),
),
];
},
body: TabBarView(
controller: _tabControllerOut,
children: const [
Tab1WithNestedTabView(),
Tab2(),
Tab3(),
],
),
),
),
);
}
}
class Tab1WithNestedTabView extends StatefulWidget {
const Tab1WithNestedTabView({Key? key}) : super(key: key);
@override
State<Tab1WithNestedTabView> createState() => _Tab1WithNestedTabViewState();
}
class _Tab1WithNestedTabViewState extends State<Tab1WithNestedTabView> with TickerProviderStateMixin {
late TabController _tabControllerIn;
@override
void initState() {
_tabControllerIn = TabController(length: 2, vsync: this);
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: [
SliverToBoxAdapter(
child: TabBar(
controller: _tabControllerIn,
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
],
),
),
SliverFillRemaining(
child: TabBarView(
controller: _tabControllerIn,
children: const [
ItemList(
color1: Colors.green,
color2: Colors.yellow,
),
ItemList(
color1: Colors.tealAccent,
color2: Colors.black54,
),
],
),
),
],
);
}
}
class Tab2 extends StatelessWidget {
const Tab2({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: const [
SliverFillRemaining(
child: ItemList(
color1: Colors.blue,
color2: Colors.yellow,
),
),
],
);
}
}
class Tab3 extends StatelessWidget {
const Tab3({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const ItemList(
color1: Colors.deepOrange,
color2: Colors.pinkAccent,
);
}
}
// Sample list
class ItemList extends StatelessWidget {
final Color color1;
final Color color2;
const ItemList({
required this.color1,
required this.color2,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
color: index.isOdd ? color1 : color2,
height: 200,
child: Text(index.toString()),
);
},
);
}
}
I admit it's not exactly the most graceful way, but worked fine enough for me. If anyone finds a better one - I'll be happy to mark that one as an accepted answer.