Skylar
Skylar

Reputation: 103

SliverAppBar with TabBar and SliverList inside CustomScrollView causes: ScrollController is currently attached to more than one ScrollPosition

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

Answers (1)

Md. Yeasin Sheikh
Md. Yeasin Sheikh

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

Related Questions