Reputation: 103
Having a SliverAppBar
with a TabBar
and a list of items, causes the scroll controller to complain.
I have implemented it in the following way,there is a NestedScrollView
that has a SliverAppBar
, a SliverPersistentHeader
that contains the TabBar
and then the body of the NestedScrollView
that contains the TabBarView
with SliverLists
inside CustomScrollViews
.
The scroll controller seems to cause issues. It says that it is attached to more than one view. So I tried adding a new scroll controller to each of the custom scroll views, but this stops the sliver app bar from collapsing when scrolling the list itself.
Apart from this, it also does not remember the scroll state when switching tabs... I have tried using the AutomaticKeepAliveClientMixin
, but this does not seem to work. Any suggestions will be so welcome on how to fix the scroll issue and the state of the scroll controller? :)
Also NOTE: I am only testing on Flutter Web, and not mobile...
I do not know whether this is a bug in my code or a bug in flutter.
See my flutter doctor below:
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel unknown, 2.5.0, on Microsoft Windows [Version 10.0.22000.434], locale en-ZA)
[!] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
X cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
X Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Android Studio (version 4.0)
[√] Connected device (2 available)
! Doctor found issues in 1 category.
The exception thrown by the scroll controller is as follows:
══╡ EXCEPTION CAUGHT BY ANIMATION LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown while notifying status listeners for AnimationController:
The provided ScrollController is currently attached to more than one ScrollPosition.
The Scrollbar requires a single ScrollPosition in order to be painted.
When the scrollbar is interactive, the associated Scrollable widgets must have unique
ScrollControllers. The provided ScrollController must be unique to a Scrollable widget.
When the exception was thrown, this was the stack:
C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 251:49 throw_
packages/flutter/src/widgets/scrollbar.dart 1315:9 <fn>
packages/flutter/src/widgets/scrollbar.dart 1338:14 [_debugCheckHasValidScrollPosition]
packages/flutter/src/widgets/scrollbar.dart 1257:14 [_validateInteractions]
packages/flutter/src/animation/listener_helpers.dart 233:27 notifyStatusListeners
packages/flutter/src/animation/animation_controller.dart 814:7 [_checkStatusChanged]
packages/flutter/src/animation/animation_controller.dart 748:5 [_startSimulation]
packages/flutter/src/animation/animation_controller.dart 611:12 [_animateToInternal]
packages/flutter/src/animation/animation_controller.dart 495:12 reverse
packages/flutter/src/widgets/scrollbar.dart 1412:37 <fn>
C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/isolate_helper.dart 48:19 internalCallback
The AnimationController notifying status listeners was:
AnimationController#25a6e(◀ 1.000)
Below is my code:
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> { // with AutomaticKeepAliveClientMixin
ScrollController _scrollController = ScrollController();
bool _isAppBarExpanded = true;
@override
void initState() {
super.initState();
_scrollController.addListener(() {
_isAppBarExpanded = _scrollController.hasClients && _scrollController.offset < (200 - kToolbarHeight);
setState(() { });
});
}
@override
Widget build(BuildContext context) {
// super.build(context); // AutomaticKeepAlive
const String title = "Floating App Bar";
return MaterialApp(
title: title,
home: Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget> [
SliverAppBar(
backgroundColor: (_isAppBarExpanded) ? Colors.white : Colors.blue,
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text(
"Collapsing Toolbar",
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
background: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(30.0),
bottomRight: Radius.circular(30.0),
),
child: Image.network(
"https://images.pexels.com/photos/396547/pexels-photo-396547.jpeg?auto=compress&cs=tinysrgb&h=350",
fit: BoxFit.cover,
),
),
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
labelColor: Colors.black87,
unselectedLabelColor: Colors.grey,
tabs: <Widget> [
Tab(
icon: Icon(Icons.info),
text: "Tab 1",
),
Tab(
icon: Icon(Icons.lightbulb_outline),
text: "Tab 2",
),
],
),
),
pinned: true,
),
];
},
body: TabBarView(
children: <Widget> [
CustomScrollView(
slivers: <Widget> [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Text("Item " + index.toString());
},
childCount: 200,
// addAutomaticKeepAlives: true,
),
),
],
),
CustomScrollView(
slivers: <Widget> [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Text("Test " + index.toString());
},
childCount: 100,
// addAutomaticKeepAlives: true,
),
),
],
),
],
),
),
),
),
);
}
// for AutomaticKeepAlive
// @override
// bool get wantKeepAlive {
// return true;
// }
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar);
final TabBar _tabBar;
@override
double get minExtent {
return _tabBar.preferredSize.height + 30;
}
@override
double get maxExtent {
return _tabBar.preferredSize.height + 30;
}
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
alignment: Alignment.center,
color: Theme.of(context).scaffoldBackgroundColor,
child: Padding(
padding: EdgeInsets.only(top: 15.0, bottom: 15.0),
child: _tabBar,
),
);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
Upvotes: 4
Views: 3249
Reputation: 63799
This problem can be solved by adding another ScrollController
on CustomScrollView
.
final ScrollController _scrollController2 = ScrollController();
....
TabBarView(
children: <Widget>[
CustomScrollView(
controller: _scrollController2,
You can also add another to next CustomScrollView
.
Upvotes: 2