Skip to content

NestedScrollView's use of PrimaryScrollController/AutomaticKeepAliveClientMixin/PageStorageKey breaks when there are multiple inner ScrollPositions #159123

Open

Description

@zmtzawqlp

Steps to reproduce

https://github.com/flutter/flutter/pull/157756/files this pr, it's not fixed those issues.

The reason is that the scroll list in NestedScrollView's body are cached, they are not removed from ScrollController, so when tap on other tab, and scroll the list, it will change all of postions in ScrollController.

Expected results

on scroll current tab list

Actual results

all of tab list are scroll

Code sample

Code sample
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<StatefulWidget> createState() {
    return MyAppState();
  }
}

class MyAppState extends State<MyApp> with TickerProviderStateMixin {
  final List<ListItem> listData = [];
  ScrollController sc = ScrollController(initialScrollOffset: 0.0);
  late TabController tc;

  @override
  void initState() {
    tc = TabController(length: 2, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: Text('Test')), body: getBody());
  }

  Widget getBody() {
    return NestedScrollView(
      controller: sc,

      body: Column(
        children: [
          TabBar(tabs: [Tab(text: 'tab1'), Tab(text: 'tab2')], controller: tc),
          Expanded(
            child: TabBarView(
              controller: tc,
              children: <Widget>[
                ListBuilder(name: Icons.abc, count: 40),
                ListBuilder(name: Icons.accessibility, count: 50),
              ],
            ),
          ),
        ],
      ),
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return [];
      },
    );
  }
}

class ListItem {
  final String title;
  final IconData iconData;

  ListItem(this.title, this.iconData);
}

class ListBuilder extends StatefulWidget {
  final int count;
  final IconData name;
  const ListBuilder({required this.count, required this.name, super.key});
  @override
  _LoadImgByLocAppPageState createState() =>
      _LoadImgByLocAppPageState(count, name);
}

class _LoadImgByLocAppPageState extends State
    with AutomaticKeepAliveClientMixin {
  IconData name;
  _LoadImgByLocAppPageState(this.count, this.name);
  int count = 20;
  final List<ListItem> listData = [];
  @override
  void initState() {
    for (int i = 0; i < count; i++) {
      listData.add(ListItem("Item$i", name));
    }

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    return SafeArea(
      top: false,
      bottom: false,
      child: Builder(
        // This Builder is needed to provide a BuildContext that is "inside"
        // the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
        // find the NestedScrollView.
        builder: (BuildContext context) {
          return CustomScrollView(
            slivers: <Widget>[
              SliverPadding(
       
                padding: const EdgeInsets.all(8.0),
                // In this example, the inner scroll view has
                // fixed-height list items, hence the use of
                // SliverFixedExtentList. However, one could use any
                // sliver widget here, e.g. SliverList or SliverGrid.
                sliver: SliverFixedExtentList(
                  // The items in this example are fixed to 48 pixels
                  // high. This matches the Material Design spec for
                  // ListTile widgets.
                  itemExtent: 48.0,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      // This builder is called for each child.
                      // In this example, we just number each list item.
                      return Text(index.toString());
                    },
                    // The childCount of the SliverChildBuilderDelegate
                    // specifies how many children this inner list
                    // has. In this example, each tab has a list of
                    // exactly 30 items, but this is arbitrary.
                    childCount: listData.length,
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓] Flutter (Channel master, 3.27.0-1.0.pre.552, on macOS 15.1 24B83 darwin-arm64, locale
    zh-Hans-CN)
    • Flutter version 3.27.0-1.0.pre.552 on channel master

Activity

  1. zmtzawqlp commented on Nov 19, 2024

    @zmtzawqlp
    Author
  2. nate-thegrate commented on Nov 19, 2024

    @nate-thegrate
    Contributor

    @zmtzawqlp thanks for reporting this! I'm able to reproduce this problem on the current master and stable branches.

    I felt that the one change likely wouldn't resolve all 5 issues; thanks for confirming that. I likely won't be able to continue the effort of fixing these bugs but am happy to help with triage.

  3. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: scrollingViewports, list views, slivers, etc.
    P2Important issues not at the top of the work list
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Nov 19, 2024
  4. yuukiw00w commented on Nov 19, 2024

    @yuukiw00w
  5. yuukiw00w commented on Dec 11, 2024

    @yuukiw00w

    Apologies to those who were expecting this fix, but I have closed my PR.

    After working on this issue, I came to understand that this behavior is part of the current NestedScrollView specification. From what I can tell by reading the code, ScrollController instances associated with a NestedScrollView are expected to remain synchronized as long as they are kept alive.

    Since I do not have plans to create an alternative NestedScrollView to achieve new behavior, I hope someone else will take on this challenge in the future.

  6. yuukiw00w commented on Dec 18, 2024

    @yuukiw00w
  7. IbrokhimMirzaev commented on Jan 25, 2025

    @IbrokhimMirzaev
  8. jlzhjp commented on Jan 26, 2025

    @jlzhjp

    For those who encountered this issue, please refer to this solution: #36419 (comment) by @maRci002 , based on #118713 (comment). The original DartPad link is broken; here is an updated link: https://dartpad.dev/?id=5d1a0c7c4b55281ccff377fc8ea90b60.

  9. jlzhjp commented on Jan 27, 2025

    @jlzhjp

    I'm following @AsturaPhoenix 's hint here #118713 (comment) to further optimize the solution and here we are.

    Video
    2025-01-28.12-32-14.mp4
    Code
    // ...
    body: Builder(
      builder: (context) {
        final controller = DefaultTabController.of(context);
        return TabBarView(
          children: [
            for (var i = 0; i < controller.length; ++i)
              ListenableBuilder(
                key: Key("Key$i"),
                listenable: controller,
                builder: (context, child) {
                  // rebuild the [SubordinateScrollControllerProvider] when the tab index changes
                  return SubordinateScrollControllerProvider(
                      // should scroll position attached by the subtree active
                      isActive: controller.index == i,
                      child: Page(
                        number: i,
                        size: 20 * (i + 1),
                      ));
                },
              ),
          ],
        );
      },
    ),
    // ...

    What's working out of box:

    • ScrollPositions do not get synchronized
    • RefreshIndicator (although the position is kind of weird)
      • Fixed using SliverOverlapAbsorberHandle.layoutExtent
    • "Scroll to top"
    • Scrollbars

    Since I'm not an expert, there is definitely something I'm missing. But I think it's the right way to go.

  10. muratgorken commented on Jun 15, 2025

    @muratgorken

    I don't mean to be rude, but the problem persists. Also
    NestedScrollView > Tabbar > Refresh indicator > Any scrollable widget
    If a tab triggers the refresh indicator, other tabs are refreshed in the same way. Moreover, in some cases just scrolling to the top can trigger this behavior. The smallest problem for me is that tabs can't carry their own scroll position. As far as I understand, this problem has been going on since 2023 and the flutter team is not working on it. This is a big problem. Almost all modern apps use tabs and use mechanisms like refresh, load on scroll, etc. in various ways. It's really sad that Flutter can't provide this naturally.

    Note: The extended_nested_scroll_view package seems to be a workaround, but it doesn't fully fix the problem and doesn't work exactly as expected.

  11. pawdom-dev commented on Jul 26, 2025

    @pawdom-dev

    I strongly urge the immediate removal of the label workaround available, as it has consistently demonstrated its ineffectiveness.

    I have a case involving a TabBar, but the scroll position keeps resetting. When using:

    NestedScrollView ( headerSliverBuilder:SliverAppBar (bottom: TabBar), body: TabBarView(children:[RefreshIndicator(child:CustomScrollView(slivers))])

    I tried make workaround work for TabBar with no success.

    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.32.6, on macOS 15.4 24E248 darwin-x64)
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
    [✓] Xcode - develop for iOS and macOS (Xcode 16.4)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2024.1)
    [✓] IntelliJ IDEA Community Edition (version 2023.3.4)
    [✓] VS Code (version 1.102.0)
    [✓] Connected device (2 available)
    [✓] Network resources
    
  12. jlzhjp commented on Jul 28, 2025

    @jlzhjp

    I strongly urge the immediate removal of the label workaround available, as it has consistently demonstrated its ineffectiveness.

    I have a case involving a TabBar, but the scroll position keeps resetting. When using:

    NestedScrollView ( headerSliverBuilder:SliverAppBar (bottom: TabBar), body: TabBarView(children:[RefreshIndicator(child:CustomScrollView(slivers))])

    I tried make workaround work for TabBar with no success.

    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.32.6, on macOS 15.4 24E248 darwin-x64)
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
    [✓] Xcode - develop for iOS and macOS (Xcode 16.4)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2024.1)
    [✓] IntelliJ IDEA Community Edition (version 2023.3.4)
    [✓] VS Code (version 1.102.0)
    [✓] Connected device (2 available)
    [✓] Network resources
    

    I haven't used Flutter in a while, but the workaround still works for me. Can you give it a try?

    https://dartpad.dev/f32d7f4d4a72de2d90d85c7ce3510115

  13. pawdom-dev commented on Jul 30, 2025

    @pawdom-dev
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: scrollingViewports, list views, slivers, etc.found in release: 3.24Found to occur in 3.24frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework teamworkaround availableThere is a workaround available to overcome the issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions