Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Statusbar tap to scroll top doesn't work in nested navigation #85603

Open
azeek opened this issue Jun 30, 2021 · 18 comments · May be fixed by #150480
Open

[iOS] Statusbar tap to scroll top doesn't work in nested navigation #85603

azeek opened this issue Jun 30, 2021 · 18 comments · May be fixed by #150480
Labels
a: fidelity Matching the OEM platforms better d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos d: stackoverflow Good question for Stack Overflow f: routes Navigator, Router, and related APIs. f: scrolling Viewports, list views, slivers, etc. found in release: 2.2 Found to occur in 2.2 found in release: 2.3 Found to occur in 2.3 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on P3 Issues that are less important to the Flutter project platform-ios iOS applications specifically team-ios Owned by iOS platform team triaged-ios Triaged by iOS platform team

Comments

@azeek
Copy link

azeek commented Jun 30, 2021

ios status bar touch scroll top move doesn't work in nested navigation.

Is there any workaround for this?

Nested navigation is used a lot in many apps such as Netflix.

With one thought, I tried the following method, but it returns null and it doesn't work.

SingleChildScrollView(
      controller: PrimaryScrollController.of(Navigator.of(context, rootNavigator: true).context),
      ...
      ...
      ...
    )
@TahaTesser TahaTesser added the in triage Presently being triaged by the triage team label Jun 30, 2021
@TahaTesser
Copy link
Member

Hi @azeek
Can you please provide your flutter doctor -v, a complete reproducible minimal code sample, and complete flutter run --verbose logs running the minimal code sample?
Thank you

@TahaTesser TahaTesser added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Jun 30, 2021
@azeek azeek closed this as completed Jul 1, 2021
@no-response no-response bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Jul 1, 2021
@azeek azeek reopened this Jul 1, 2021
@azeek
Copy link
Author

azeek commented Jul 1, 2021

Thank you . @TahaTesser

this is my 'flutter doctor -v' result

[✓] Flutter (Channel stable, 2.2.2, on macOS 11.4 20F71 darwin-x64, locale ko-KR)
    • Flutter version 2.2.2 at /Users/azeek/sdk/flutter
    • Framework revision d79295af24 (3 weeks ago), 2021-06-11 08:56:01 -0700
    • Engine revision 91c9fc8fe0
    • Dart version 2.13.3

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/azeek/Library/Android/sdk
    • Platform android-30, build-tools 30.0.2
    • Java binary at: /Users/azeek/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/202.7351085/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.5.1, Build version 12E507
    • CocoaPods version 1.10.1

[✓] Android Studio (version 4.2)
    • Android Studio at /Users/azeek/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/202.7351085/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)

[✓] IntelliJ IDEA Ultimate Edition (version 2021.1.3)
    • IntelliJ at /Users/azeek/Applications/JetBrains Toolbox/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 57.0.5
    • Dart plugin version 211.7665

[✓] VS Code (version 1.57.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.23.0

[✓] Connected device (1 available)
    • iPhone (mobile) •********************************************** • ios • iOS 14.6

and this is example code.
https://gist.github.com/azeek/0dcacd0f08025ce278f25de714ce6dd1

This is a test video.
https://youtu.be/Q3A_KB_Ru-E

I think it has something to do with this.
#74727 (comment)
#69795

Hi @azeek
Can you please provide your flutter doctor -v, a complete reproducible minimal code sample, and complete flutter run --verbose logs running the minimal code sample?
Thank you

@TahaTesser TahaTesser changed the title ios status bar touch scroll top move doesn't work in nested navigation. [iOS] Statusbar tap to scroll top doesn't work in nested navigation Jul 1, 2021
@TahaTesser
Copy link
Member

Hi @azeek
Thank you for the code sample and clip

This is most likely because nested navigator view isn't getting informed this event has occurred (status bar is tapped)

Used the following minimal code sample

minimal code sample
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  final _navigatorKey = GlobalKey<NavigatorState>();
  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: IndexedStack(
        index: currentIndex,
        children: [
          TapBodyWidget(listName: 'Tap 1'),
          Navigator(
            key: _navigatorKey,
            initialRoute: '/',
            onGenerateRoute: (settings) => MaterialPageRoute<dynamic>(
              builder: (context) {
                return TapBodyWidget(listName: 'Tap2');
              },
              settings: settings,
            ),
          )
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        unselectedItemColor: Colors.black,
        selectedItemColor: Colors.blue,
        selectedLabelStyle: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
        unselectedLabelStyle: TextStyle(
          color: Colors.black,
          fontWeight: FontWeight.bold,
        ),
        currentIndex: currentIndex,
        onTap: (index) => setState(() => currentIndex = index),
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.favorite_border), label: 'tab 1'),
          BottomNavigationBarItem(icon: Icon(Icons.bookmark), label: 'tab 2'),
        ],
      ),
    );
  }
}

class TapBodyWidget extends StatelessWidget {
  const TapBodyWidget({Key? key, required this.listName}) : super(key: key);
  final String listName;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (context, index) => Text('$listName :  $index'),
        separatorBuilder: (context, index) => Divider(),
        itemCount: 100);
  }
}
stable master

Check flutter doctor -v outputs for each channel below

flutter doctor -v
[✓] Flutter (Channel stable, 2.2.2, on macOS 11.4 20F71 darwin-x64, locale en-US)
    • Flutter version 2.2.2 at /Users/tahatesser/Code/flutter_stable
    • Framework revision d79295af24 (3 weeks ago), 2021-06-11 08:56:01 -0700
    • Engine revision 91c9fc8fe0
    • Dart version 2.13.3

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Volumes/Extreme/SDK
    • Platform android-30, build-tools 30.0.3
    • ANDROID_HOME = /Volumes/Extreme/SDK
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Volumes/Extreme/Xcode.app/Contents/Developer
    • Xcode 12.5, Build version 12E262
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)

[✓] VS Code (version 1.57.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.23.0

[✓] Connected device (4 available)
    • IN2011 (mobile)        • c9d8ee0c                             • android-arm64  • Android 11 (API 30)
    • iPhone 12 Pro (mobile) • 4819C924-80DB-4DE5-9093-71A234590ABB • ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-5
      (simulator)
    • macOS (desktop)        • macos                                • darwin-x64     • macOS 11.4 20F71 darwin-x64
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 91.0.4472.114

• No issues found!
[✓] Flutter (Channel master, 2.3.0-17.0.pre.608, on macOS 11.4 20F71 darwin-x64, locale en-US)
    • Flutter version 2.3.0-17.0.pre.608 at /Users/tahatesser/Code/flutter_master
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 18ccfe6386 (6 hours ago), 2021-07-01 05:26:03 +0200
    • Engine revision eab0cd490a
    • Dart version 2.14.0 (build 2.14.0-262.0.dev)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Volumes/Extreme/SDK
    • Platform android-30, build-tools 30.0.3
    • ANDROID_HOME = /Volumes/Extreme/SDK
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Volumes/Extreme/Xcode.app/Contents/Developer
    • Xcode 12.5, Build version 12E262
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)

[✓] VS Code (version 1.57.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.23.0

[✓] Connected device (4 available)
    • IN2011 (mobile)        • c9d8ee0c                             • android-arm64  • Android 11 (API 30)
    • iPhone 12 Pro (mobile) • 4819C924-80DB-4DE5-9093-71A234590ABB • ios            • com.apple.CoreSimulator.SimRuntime.iOS-14-5
      (simulator)
    • macOS (desktop)        • macos                                • darwin-x64     • macOS 11.4 20F71 darwin-x64
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 91.0.4472.114

• No issues found!

✅ : No Issue ❌: Issue reproduced

@TahaTesser TahaTesser added a: fidelity Matching the OEM platforms better f: routes Navigator, Router, and related APIs. f: scrolling Viewports, list views, slivers, etc. found in release: 2.2 Found to occur in 2.2 found in release: 2.3 Found to occur in 2.3 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on platform-ios iOS applications specifically and removed in triage Presently being triaged by the triage team labels Jul 1, 2021
@goderbauer
Copy link
Member

/cc @Piinks

@Piinks Piinks added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos d: stackoverflow Good question for Stack Overflow documentation labels Jul 8, 2021
@Piinks
Copy link
Contributor

Piinks commented Jul 8, 2021

Hi @azeek thanks for reporting.
I think there are a couple options here, and definitely some room to improve our documentation. :)

First, the status bar tap & scroll animation is controlled by the Scaffold. When the Scaffold detects this gesture, it looks up the PrimaryScrollController of the current context and animates it to the top.
A PrimaryScrollController is created by default for each ModalRoute, so in your sample, when you tap on the status bar, Tap 1 works, but the Scaffold is not using the same PrimaryScrollController your Tap 2 ListView is attached to.

Super simplified:

Route
  - PrimaryScrollController // Connected to Tap 1, used by Scaffold below
    - Scaffold
      - ListView // Tap 1 - connected to the above controller
      - Route // From nested Navigator
        - PrimaryScrollController // Connected to Tap 2
          - ListView // Tap 2

Passing the right PrimaryScrollController down to Tap 2 appears to be the easiest way to resolve this. I've updated your sample below. :)

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  final _navigatorKey = GlobalKey<NavigatorState>();
  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
   // Grab the top most controller
    final ScrollController controller = PrimaryScrollController.of(context)!;
    return Scaffold(
      appBar: AppBar(),
      body: IndexedStack(
        index: currentIndex,
        children: [
          TapBodyWidget(listName: 'Tap 1b'),
          Navigator(
            key: _navigatorKey,
            initialRoute: '/',
            onGenerateRoute: (settings) => MaterialPageRoute<dynamic>(
              builder: (context) {
                // Pass it down :)
                return TapBodyWidget(listName: 'Tap 2b', controller: controller);
              },
              settings: settings,
            ),
          )
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        unselectedItemColor: Colors.black,
        selectedItemColor: Colors.blue,
        selectedLabelStyle: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
        unselectedLabelStyle: TextStyle(
          color: Colors.black,
          fontWeight: FontWeight.bold,
        ),
        currentIndex: currentIndex,
        onTap: (index) => setState(() => currentIndex = index),
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.favorite_border), label: 'tab 1'),
          BottomNavigationBarItem(icon: Icon(Icons.bookmark), label: 'tab 2'),
        ],
      ),
    );
  }
}

class TapBodyWidget extends StatelessWidget {
  const TapBodyWidget({Key? key, required this.listName, this.controller}) : super(key: key);
  final String listName;
  final ScrollController? controller;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        controller: controller,
        itemBuilder: (context, index) => Text('$listName :  $index'),
        separatorBuilder: (context, index) => Divider(),
        itemCount: 100);
  }
}

@azeek
Copy link
Author

azeek commented Jul 9, 2021

Hi @azeek thanks for reporting.
I think there are a couple options here, and definitely some room to improve our documentation. :)

First, the status bar tap & scroll animation is controlled by the Scaffold. When the Scaffold detects this gesture, it looks up the PrimaryScrollController of the current context and animates it to the top.
A PrimaryScrollController is created by default for each ModalRoute, so in your sample, when you tap on the status bar, Tap 1 works, but the Scaffold is not using the same PrimaryScrollController your Tap 2 ListView is attached to.

Super simplified:

Route
  - PrimaryScrollController // Connected to Tap 1, used by Scaffold below
    - Scaffold
      - ListView // Tap 1 - connected to the above controller
      - Route // From nested Navigator
        - PrimaryScrollController // Connected to Tap 2
          - ListView // Tap 2

Passing the right PrimaryScrollController down to Tap 2 appears to be the easiest way to resolve this. I've updated your sample below. :)

Code

Thank you

It was resolved in that way.

It doesn't seem like a very good structure in terms of logic to keep passing ScrollController whenever the page continues to move in nested navigation.

Wouldn't it be better to bring the root navigator scroll controller to the nested navigation in the following way to configure the logic with weak coupling as follows?

ex)

PrimaryScrollController.of(Navigator.of(context, rootRoute: true).context)

@Piinks
Copy link
Contributor

Piinks commented Jul 9, 2021

Wouldn't it be better to bring the root navigator scroll controller to the nested navigation in the following way to configure the logic with weak coupling as follows?

ex)

PrimaryScrollController.of(Navigator.of(context, rootRoute: true).context)

Sure! Like said, there is more than one way to resolve. :) We can certainly include this in our documentation as an example.

@LuckyuGame
Copy link

LuckyuGame commented Aug 29, 2021

When using the 'PrimaryScrollController' in nested navigation, the pushed screen also scrolls up, but the previous screen also scrolls up.

How do I prevent the previous screen from scrolling?

Demo

My Code 👇

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  final _navigatorKey = GlobalKey<NavigatorState>();
  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    // Grab the top most controller
    final ScrollController controller = PrimaryScrollController.of(context)!;

    return Scaffold(
      body: IndexedStack(
        index: currentIndex,
        children: [
          TapBodyWidget(
            listName: 'A',
          ),
          Navigator(
            key: _navigatorKey,
            initialRoute: 'B',
            onGenerateRoute: (settings) => MaterialPageRoute<dynamic>(
              builder: (context) {
                return TapBodyWidget(listName: 'B', controller: controller);
              },
              settings: settings,
            ),
          )
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        unselectedItemColor: Colors.black,
        selectedItemColor: Colors.blue,
        selectedLabelStyle: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
        unselectedLabelStyle: TextStyle(
          color: Colors.black,
          fontWeight: FontWeight.bold,
        ),
        currentIndex: currentIndex,
        onTap: (index) => setState(() => currentIndex = index),
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite_border),
            label: 'TAB A',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.bookmark),
            label: 'TAB B',
          ),
        ],
      ),
    );
  }
}

class TapBodyWidget extends StatelessWidget {
  const TapBodyWidget({Key? key, required this.listName, this.controller})
      : super(key: key);
  final String listName;
  final ScrollController? controller;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: ListView.separated(
          controller: controller,
          itemBuilder: (context, index) {
            return ListTile(
              tileColor: Colors.red[100 * (index % 9 + 1)],
              title: Text('$listName :  $index'),
              trailing: Icon(Icons.chevron_right),
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => TapBodyWidget(
                    listName: '$listName:$index',
                    controller: controller,
                  ),
                ),
              ),
            );
          },
          separatorBuilder: (context, index) => Divider(),
          itemCount: 100),
    );
  }
}

@azeek
Copy link
Author

azeek commented Sep 13, 2021

Additionally, if there are multiple tabs using the bottomNavigationBar, all other inactive tabs will scroll to the top if you tap the ios status bar.

@SuhwanCha
Copy link
Contributor

SuhwanCha commented Dec 27, 2021

https://medium.com/@suhw4n/flutter-tab-to-scroll-top-on-nested-navigation-b176cc759921

It's my more simple solution then pass primary scroll controller, but it still has @goderbauer @LuckyuGame 's problem.
To solve prevent to scrolled other screen together, you maybe override _handleStatusBarTap methods on Scaffold

scaffold.dart

  void _handleStatusBarTap() {
    final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
    if (_primaryScrollController != null && _primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
      );
    }
  }

Before _primaryScrollController.animateTo executed, you may check some status (i.e. active index or so on), and prevent scrolled

@mark8044
Copy link

mark8044 commented Jan 2, 2022

https://medium.com/@suhw4n/flutter-tab-to-scroll-top-on-nested-navigation-b176cc759921

It's my more simple solution then pass primary scroll controller, but it still has @goderbauer @LuckyuGame 's problem. To solve prevent to scrolled other screen together, you maybe override _handleStatusBarTap methods on Scaffold

scaffold.dart

  void _handleStatusBarTap() {
    final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
    if (_primaryScrollController != null && _primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
      );
    }
  }

Before _primaryScrollController.animateTo executed, you may check some status (i.e. active index or so on), and prevent scrolled

I implemented this fix and it does work if you only have 1 tab kind of application.

But when using an application with multiple tab bars and with list views on each of those tabs, this doesn't work due to errors with the scrollview being attached to several different list views ScrollController attached to multiple scroll views.

Furthermore, a tap on the status bar will bring all the list views in all the tabs back up to the top simultaneously.

But this does seem like a step in the right direction...

@SuhwanCha
Copy link
Contributor

https://medium.com/@suhw4n/flutter-tab-to-scroll-top-on-nested-navigation-b176cc759921

It's my more simple solution then pass primary scroll controller, but it still has @goderbauer @LuckyuGame 's problem. To solve prevent to scrolled other screen together, you maybe override _handleStatusBarTap methods on Scaffold

scaffold.dart

  void _handleStatusBarTap() {
    final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
    if (_primaryScrollController != null && _primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
      );
    }
  }

Before _primaryScrollController.animateTo executed, you may check some status (i.e. active index or so on), and prevent scrolled

I implemented this fix and it does work if you only have 1 tab kind of application.

But when using an application with multiple tab bars and with list views on each of those tabs, this doesn't work due to errors with the scrollview being attached to several different list views ScrollController attached to multiple scroll views.

Furthermore, a tap on the status bar will bring all the list views in all the tabs back up to the top simultaneously.

But this does seem like a step in the right direction...

Yes.. it's problem in this solution. How about subscribe tab bar status and dynamically detach&attach scroll controller?

@mark8044
Copy link

mark8044 commented Jan 2, 2022

https://medium.com/@suhw4n/flutter-tab-to-scroll-top-on-nested-navigation-b176cc759921
It's my more simple solution then pass primary scroll controller, but it still has @goderbauer @LuckyuGame 's problem. To solve prevent to scrolled other screen together, you maybe override _handleStatusBarTap methods on Scaffold
scaffold.dart

  void _handleStatusBarTap() {
    final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
    if (_primaryScrollController != null && _primaryScrollController.hasClients) {
      _primaryScrollController.animateTo(
        0.0,
        duration: const Duration(milliseconds: 300),
        curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
      );
    }
  }

Before _primaryScrollController.animateTo executed, you may check some status (i.e. active index or so on), and prevent scrolled

I implemented this fix and it does work if you only have 1 tab kind of application.
But when using an application with multiple tab bars and with list views on each of those tabs, this doesn't work due to errors with the scrollview being attached to several different list views ScrollController attached to multiple scroll views.
Furthermore, a tap on the status bar will bring all the list views in all the tabs back up to the top simultaneously.
But this does seem like a step in the right direction...

Yes.. it's problem in this solution. How about subscribe tab bar status and dynamically detach&attach scroll controller?

That sounds way to complex and doesn't seem like the correct way of dealing with this.

You know what is interesting: webview_flutter https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter doesn't have this problem at all. No matter where you place it in hierarchy and no matter what navigator it is in, the scroll to top always works. It would seem like however it is able to do that, is the solution

@SuhwanCha
Copy link
Contributor

@mark8044
I totally agree with you. I knew it works on third party plugin: https://github.com/pichillilorenzo/flutter_inappwebview
I’ll try to find another solution and if needed, I’ll propose about this problem :)

In Scrollable widget, primary parameter only do use primary scroll controller as scroll controller, and scrolls to top feature is not implemented on iOS native, but on GestureDetector in Scaffold. So, I can’t even understand why scrolls to top doesn’t work in nested navigation, because in nested navigation, there will be another Scaffold and there are another PrimaryScollController.

I think we need to prune PrimaryScrollController and find another solution.

@fushihara
Copy link

I will share the solution I just found.
I was able to handle direct touch events by making the first Scaffold widget a stack and adding a GestureDetector under the Scaffold.

I have confirmed that it works with a simple screen transition, but it has only been a few minutes since I found this, so there is probably some kind of problem!

I'll take it down if there are any problems, so please let me know if you find it not working correctly.

@bschmalb
Copy link

Still no updates on this issue right? 😕

@EArminjon
Copy link
Contributor

EArminjon commented Aug 23, 2023

I found a little trick to allow status bar scroll to top for complex nested navigation.

I use the solution of Piinks but I go forward to bypass a limitation when you want to use a custom scrollController. As the PrimaryScrollController will be used by many lists in the app, I can't listen to it individually to perform some scroll operation.

My app architecture :

Scaffold // I give a GlobalKey and set a global getter to get the PrimaryScrollController
 - IndexedStack
   - Navigator // In our app, each IndexedStack item have it.
     - MaterialPageRoute
       - Scaffold
         - Navigator
           - Scaffold
             - ListView / CustomScrollView // use the PrimaryScrollController global as controller

You need to define two globals (or something else) :

// Key for the first scaffold
final GlobalKey<ScaffoldState> mainScaffoldKey = GlobalKey<ScaffoldState>();

// Used for all scrollView which want to allow scroll to top feature
ScrollController get primaryScroll => PrimaryScrollController.of(mainScaffoldKey.currentContext!);

Simple usage :

 @override
 Widget build(BuildContext context) {
   return ListView(
    controller: primaryScroll,
   );
 }

If you want to use a custom scrollController in a scrollView but also want to allow scroll to top on it you will need to register this custom scrollController to the 'primaryScroll' controller defined above.

final ScrollController scrollController = ScrollController();

 // Used for dispose purpose
 ScrollPosition? _position;
 ScrollController? _primaryController;

@override
 void initState() {
   super.initState();
   // Used to sync the given scroll to the main primary one for the scroll to top iOS feature
   WidgetsBinding.instance.addPostFrameCallback((_) {
     _position = scrollController.position;
     _primaryController = primaryScroll;
     _primaryController!.attach(_position!);
   });
   
   // As we have a dedicated controller, we can listen to it and read offset / position without issues
   // You can listen, if you want, to the controller as before :
   // scrollController.addListener(() {});
 }
 
 @override
 void dispose() {
   super.dispose();
   if (_primaryController != null && _position != null) {
     _primaryController!.detach(_position!);
   }
   scrollController.dispose();
 }
 
 @override
 Widget build(BuildContext context) {
   return ListView(
    controller: scrollController,
   );
 }

This solution to use a custom scrollController is not sexy. It works but I still continue to search a better solution.
With this trick i can let my user use the scroll to top feature in nested navigation :).

reedom added a commit to reedom/scrolls_to_top that referenced this issue Mar 15, 2024
Under nested navigation tree, it could invoke callbacks of
multiple widget.

Related issue: flutter/flutter#85603
reedom added a commit to reedom/scrolls_to_top that referenced this issue Mar 15, 2024
Under a nested navigation tree, it could invoke callbacks of
multiple widgets.

Related issue: flutter/flutter#85603
@SuhwanCha SuhwanCha linked a pull request Jun 19, 2024 that will close this issue
9 tasks
@aidarbye
Copy link

Im using go_router and StatefulShellRoute.indexedStack, and the behaviour is like @LuckyuGame, but it can be fixed with @EArminjon solution with custom scroll controller but a little bit edited, something like scrolls_to_top package with providing callback for onScrollsToTop when you can check if this widget are visible right now, using go_router currentConfiguration last path or VisibilityDetector from google.dev, it will work, atleast it works but i hope it temporary solution😗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: fidelity Matching the OEM platforms better d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos d: stackoverflow Good question for Stack Overflow f: routes Navigator, Router, and related APIs. f: scrolling Viewports, list views, slivers, etc. found in release: 2.2 Found to occur in 2.2 found in release: 2.3 Found to occur in 2.3 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on P3 Issues that are less important to the Flutter project platform-ios iOS applications specifically team-ios Owned by iOS platform team triaged-ios Triaged by iOS platform team
Projects
None yet