-
Notifications
You must be signed in to change notification settings - Fork 26.8k
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
Comments
Hi @azeek |
Thank you . @TahaTesser this is my 'flutter doctor -v' result
and this is example code. This is a test video. I think it has something to do with this.
|
Hi @azeek 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 sampleimport '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);
}
}
Check 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 |
/cc @Piinks |
Hi @azeek thanks for reporting. 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. 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 Codeimport '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);
}
} |
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) |
Sure! Like said, there is more than one way to resolve. :) We can certainly include this in our documentation as an example. |
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? 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),
);
}
}
|
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. |
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. 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 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: |
@mark8044 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. |
I will share the solution I just found. 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. |
Still no updates on this issue right? 😕 |
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. |
Under nested navigation tree, it could invoke callbacks of multiple widget. Related issue: flutter/flutter#85603
Under a nested navigation tree, it could invoke callbacks of multiple widgets. Related issue: flutter/flutter#85603
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😗 |
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.
The text was updated successfully, but these errors were encountered: