How to make nested NestedScrollViews work? - flutter

When trying to use the NestedScrollView with a ListView inside a different NestedScrollView Flutter throws a stack overflow error:
════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building PrimaryScrollController(no controller):
Stack Overflow
Here's a minimal-ish code where it happens:
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const NestedScrollView1();
}
}
class NestedScrollView1 extends StatelessWidget {
const NestedScrollView1({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: NestedScrollView(
physics: const ClampingScrollPhysics(),
headerSliverBuilder: (_, __) => [
SliverToBoxAdapter(
child: Container(
color: Colors.blue,
height: 100,
),
)
],
body: NestedScrollView2(),
),
);
}
}
class NestedScrollView2 extends StatelessWidget {
final ScrollController scrollController = ScrollController();
NestedScrollView2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return NestedScrollView(
controller: PrimaryScrollController.of(context),
physics: const ClampingScrollPhysics(),
headerSliverBuilder: (ctx, __) => [
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 100,
),
),
],
body: const ListOfItems(),
);
}
}
class ListOfItems extends StatelessWidget {
const ListOfItems({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView(
physics: const ClampingScrollPhysics(),
// controller: PrimaryScrollController.of(context),
children: [
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
],
);
}
}
if you uncomment the controller line in ListView - it throws a stack overflow like this:
════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building ListView(scrollDirection: vertical, _NestedScrollController#81c19(inner, one client, offset 0.0), ClampingScrollPhysics, dependencies: [MediaQuery]):
Stack Overflow
The relevant error-causing widget was
ListView
Thing is on my project I have a page with a TabBarView and one of its sections has a TabBarView of it's own, and I wanted to use the NestedScrollView's to hold the tabs inside headerSliverBuilder's. Is there any way to go around this, without telling designer to reconsider the page UI or building complex custom scroll logic?
Edit: for clarity, adding a draw.io screenshot of the layout I'm trying to achieve (cannot put images right into the posts yet, ugh).

(Forgot to post an answer, better late than never I hope)
I have managed to achieve what I needed in a hack-ish solution from my colleague of just using CustomScrollView with the nested TabBar and ListView inside of SliverFillRemaining:
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: OuterTabView(),
);
}
}
class OuterTabView extends StatefulWidget {
const OuterTabView({Key? key}) : super(key: key);
#override
State<OuterTabView> createState() => _OuterTabViewState();
}
class _OuterTabViewState extends State<OuterTabView> with TickerProviderStateMixin {
late TabController _tabControllerOut;
#override
void initState() {
super.initState();
_tabControllerOut = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (_, __) {
return <Widget>[
SliverToBoxAdapter(
child: TabBar(
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
SizedBox(height: 40),
],
controller: _tabControllerOut,
),
),
];
},
body: TabBarView(
controller: _tabControllerOut,
children: const [
Tab1WithNestedTabView(),
Tab2(),
Tab3(),
],
),
),
),
);
}
}
class Tab1WithNestedTabView extends StatefulWidget {
const Tab1WithNestedTabView({Key? key}) : super(key: key);
#override
State<Tab1WithNestedTabView> createState() => _Tab1WithNestedTabViewState();
}
class _Tab1WithNestedTabViewState extends State<Tab1WithNestedTabView> with TickerProviderStateMixin {
late TabController _tabControllerIn;
#override
void initState() {
_tabControllerIn = TabController(length: 2, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: [
SliverToBoxAdapter(
child: TabBar(
controller: _tabControllerIn,
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
],
),
),
SliverFillRemaining(
child: TabBarView(
controller: _tabControllerIn,
children: const [
ItemList(
color1: Colors.green,
color2: Colors.yellow,
),
ItemList(
color1: Colors.tealAccent,
color2: Colors.black54,
),
],
),
),
],
);
}
}
class Tab2 extends StatelessWidget {
const Tab2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: const [
SliverFillRemaining(
child: ItemList(
color1: Colors.blue,
color2: Colors.yellow,
),
),
],
);
}
}
class Tab3 extends StatelessWidget {
const Tab3({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ItemList(
color1: Colors.deepOrange,
color2: Colors.pinkAccent,
);
}
}
// Sample list
class ItemList extends StatelessWidget {
final Color color1;
final Color color2;
const ItemList({
required this.color1,
required this.color2,
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
color: index.isOdd ? color1 : color2,
height: 200,
child: Text(index.toString()),
);
},
);
}
}
I admit it's not exactly the most graceful way, but worked fine enough for me. If anyone finds a better one - I'll be happy to mark that one as an accepted answer.

Related

How To Scroll To A Specific Widget in Flutter Using SingleChildScrollView

I have a MainScreen and SecondScreen. When the drawer item in the MainScreen clicked. It should move to SecondScreen Container widget. But how to do that?
I have set ScrollController for SecondScreen SingleChildScrollView. but how to move to a certain widget?
Create a method in SecondScreen which scroll to the widget?
What if I have 3rd screen which need same functionality.
SecondScreen.dart
import 'package:flutter/material.dart';
ScrollController scrollController = ScrollController();
var containerKey = GlobalKey();
class SecondScreen extends StatefulWidget {
final Key widgetKey;
const SecondScreen({Key key, this.widgetKey}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
#override
void initState() {
// TODO: implement initState
super.initState();
Scrollable.ensureVisible(
widget.widgetKey,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
Text('hi'),
Container(
color: Colors.red,
height: 1000,
),
Container(
color: Colors.green,
height: 1000,
),
Container(
key: containerKey,
color: Colors.green,
height: 1000,
),
],
),
),
);
}
}
mainscreen.dart
import 'package:flutter/material.dart';
import 'package:stackoverflow_check/scrollcheck/second_screen.dart';
class MainScreen extends StatelessWidget {
const MainScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: Drawer(
child: ListView(
children: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondScreen(widgetKey: containerKey),
),
);
//scrollController.an
},
child: Text('click'),
)
],
),
),
);
}
}
Main screen
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: Drawer(
child: ListView(
children: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondScreen(widgetNum: 2),
),
);
//scrollController.an
},
child: Text('click'),
)
],
),
),
);
}
}
SecondScreen
class SecondScreen extends StatefulWidget {
final int widgetNum;
const SecondScreen({Key? key, required this.widgetNum}) : super(key: key);
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
ScrollController scrollController = ScrollController();
var containerKey = GlobalKey();
var container2Key = GlobalKey();
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
Future.delayed(Duration(milliseconds: 100), () {
if (widget.widgetNum == 1) {
Scrollable.ensureVisible(
containerKey.currentContext!,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
} else {
Scrollable.ensureVisible(
container2Key.currentContext!,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
Text('hi'),
Container(
color: Colors.red,
height: 1000,
),
Container(
color: Colors.green,
height: 1000,
),
Container(
key: containerKey,
color: Colors.green,
height: 1000,
),
Container(
key: container2Key,
color: Colors.blue,
height: 1000,
),
],
),
),
);
}
}

Flutter: Custom widget with PageView physics and interactions

I need to make a screen on which the interaction is similar to PageView, but with something very different on the screen. I don't want to deal with separate pages. I only need the current scroll position to place my widgets as I desire.
I succeeded in achieving this by placing PageView with empty pages at the bottom of the Stack and placing my widgets inside AnimatedBuilder above PageView. The only problem is that my widgets have their own GestureDetectors and that's why PageView doesn't scroll when I start scrolling gestures over my widget.
Here is a sample. I need the same behavior but without scroll problems when I start dragging over my top widgets.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _pageController = PageController(viewportFraction: 0.7, initialPage: 5);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
PageView.builder(
controller: _pageController,
itemBuilder: (context, index) => Container(),
itemCount: 10,
),
for (double angleOffset in [0.25, 0.5, 0.75, 1])
AnimatedBuilder(
animation: _pageController,
builder: (context, child) {
return Center(
child: Transform.rotate(
angle: (_pageController.page ?? 5) + pi * angleOffset,
child: Transform.translate(
offset: const Offset(0, 150),
child: const ColorContainer(),
),
),
);
},
),
],
),
);
}
}
class ColorContainer extends StatefulWidget {
const ColorContainer({Key? key}) : super(key: key);
#override
State<ColorContainer> createState() => _ColorContainerState();
}
class _ColorContainerState extends State<ColorContainer> {
double hue = 30;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => hue = (hue + 80.0) % 360),
child: Container(
width: 100,
height: 100,
decoration: ShapeDecoration(
shape: const CircleBorder(),
color: HSLColor.fromAHSL(1, hue, 1, 0.5).toColor(),
),
),
);
}
}

Flutter scrollbar that is in horizontal scrollview doesnt show correctly inner scrollview

So, I have bigger scroll view that scrolls horizontally, and inside - little box (red color) and smaller scrollview (orange color) that scrolls vertically.
There are two scrollbars on the bigger scrollview (1 - for horizontal), and second - for vertical inner.
And the problem - vertical scrollbar doesnt look right, because it can go only like blue arrow shows, and I want it to have either full height of the bigger scrollview, or be right near scrollable vertical part, but then dont hide itself if scrolled in horizontal direction.
Run on dartPad
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: ScrollSizingWidget(),
),
),
);
}
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({
Key? key,
}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController();
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scrollbar(
controller: _vertical,
notificationPredicate: (notification) => notification.depth == 1,
child: Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
child: SizedBox(
height: 500,
width: 1000,
child: Column(
children:[
Container(width: 1000, height: 200, color: Colors.green),
Flexible(
child: SingleChildScrollView(
controller: _vertical,
child: Container(
height: 700,
width: 1000,
color: Colors.yellow,
)
)
),
]
)
),
),
),
);
}
}
I have used your code to reproduce the issue. If I understood your needs right, here is the fix:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: SingleChildScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
child: Column(
children: [
Container(
height: 200,
width: 1000,
color: Colors.red,
),
Container(
height: 1000,
width: 1000,
color: Colors.orange,
),
],
),
),
),
),
),
);
}
Firstly, you say that your main SingleChildScrollView scrolls horizontally, but your widget tree starts with a Scrollbar which uses a vertical ScrollController. So you should create your widgets step by step, as you explained.
Also, since you want to see the vertical Scrollbar through your main SingleChildScrollView, I wrapped both(red and orange Containers) with Scrollbar and SingleChildScrollView to have the effect you want. Furthermore, I connected these Scrollbar and SingleChildScrollView with the same horizontal ScrollController. So now, not only the orange Container, but both are scrollable and stick together, not independent.
If you don't want the red Container being scrolled along with the orange Container, check this:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Column(
children: [
Container(
height: 200,
width: 1000,
color: Colors.red,
),
Expanded(
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: SingleChildScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
child: Container(
height: 700,
width: 1000,
color: Colors.orange,
),
),
),
),
],
),
),
),
);
}
Lastly, Scrollbar's position in iOS is a bit buggy because of the notch, etc. So I wrapped your ScrollSizingWidget with SafeArea to fix the issue in iOS.
If these answers are not what you expect, please don't hesitate to write.
Edit: After your explanations in the comments below, I have created another fix. I believe CustomScrollView and Sliver widgets are fits here perfectly. The red Container, which you want to stay in its position, should be wrapped with the SliverAppBar. Lastly, the orange Container, which you want to be able to scroll vertically, could be wrapped with SliverFixedExtentList. Please check the code below:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: CustomScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
slivers: [
SliverAppBar(
toolbarHeight: 200.0,
collapsedHeight: 200.0,
pinned: true,
stretch: true,
elevation: 0.0,
backgroundColor: Colors.transparent,
title: Container(
height: 200.0,
color: Colors.red,
),
titleSpacing: 0,
),
SliverFixedExtentList(
itemExtent: 1200.0,
delegate: SliverChildBuilderDelegate(
(_, __) => Container(
color: Colors.orange,
),
childCount: 1,
),
),
],
),
),
),
),
);
}

How to make a flutter app scroll both horizontally and vertically

I'm having a hard time making a flutter screen be scrollable both horizontally and vertically. I can make it work in one direction but fail when I try it in both directions.
Here is a mock app. For background, I'm targeting the web, where the information should be accessible by keyboard & mouse even if it's outside the immediate viewing area.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
scrollDirection: Axis.horizontal,
children: [
ListView(
scrollDirection: Axis.vertical,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: [
for (var i = 0; i < 4; i++)
Container(
child: Text('Header $i'),
width: 300,
height: 100,
color: Colors.greenAccent,
margin: const EdgeInsets.all(20)),
],
),
...[
for (var i = 0; i < 50; i++)
Container(
child: Text('Column $i'),
width: 150,
height: 50,
color: Colors.orange,
margin: const EdgeInsets.all(10),
)
],
],
)
],
)
],
),
);
}
}
Thank you for any suggestions on how to make this work!
Tony
Maybe you can try InteractiveViewer widget and Table widget
InteractiveViewer flutter Official example:
https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart
Still not exactly what I want, but it's closer and may be good enough.
I added a scroll controller.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late ScrollController _scrollControllerH;
#override
void initState() {
_scrollControllerH = ScrollController();
super.initState();
}
#override
void dispose() {
_scrollControllerH.dispose();
super.dispose();
}
/// scroll only the header independent of the table
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
scrollDirection: Axis.vertical,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Scrollbar(
controller: _scrollControllerH,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _scrollControllerH,
child: Row(
children: [
for (var i = 0; i < 4; i++)
Container(
child: Text('Header $i'),
width: 300,
height: 100,
color: Colors.greenAccent,
margin: const EdgeInsets.all(20)),
],
),
),
),
...[
for (var i = 0; i < 50; i++)
Container(
child: Text('Column $i'),
width: 150,
height: 50,
color: Colors.orange,
margin: const EdgeInsets.all(10),
)
],
],
)
],
));
}
}
You can make any view scrollable both vertically and horizontally in Flutter using the following snippet (tested and confirmed to work on a Linux app using Flutter 3.0.1):
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: <widget-to-be-scrollable-both-vertically-and-horizontally>,
),
)

Flutter web tabbar scroll issue with non primary scrollcontroller

In continuation with question
The solution provided above is good. But hard for me to implement in my project.
Expected results:
I've created two tabs.
In each tab I have SingleChildScrollView wrapped with Scrollbar.
I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
Widgets in both the tabs should be scrollabale using keyboard and mouse.
Actual results:
For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn't respond to keyboard page up /down keys.
When keyboard keys are used in Tab TWO actually contents of tab ONE are getting scrolled.
Check code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TabExample(),
);
}
}
class TabExample extends StatefulWidget {
const TabExample({Key key}) : super(key: key);
#override
_TabExampleState createState() => _TabExampleState();
}
class _TabExampleState extends State<TabExample> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
WidgetC(),
WidgetD(),
],
),
),
);
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.blue,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.green,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
This has been accepted as a bug in flutter.
Pl follow for progress here: https://github.com/flutter/flutter/issues/83711
Note for other developers facing same issue.
To overcome the mentioned problem, I changed my design layout. Instead of tabbar view I used Navigationrail widget. This solved my problem.
NavigationRail widget allowed me to attach primary scroll controller to multiple widgets without giving me exception: "ScrollController attached to multiple scroll views."
Sample code.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
WidgetC _widgetC = WidgetC();
WidgetD _widgetD = WidgetD();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('NavigationRail Demo'), centerTitle: true),
body: Row(
children: <Widget>[
NavigationRail(
elevation: 8.0,
selectedIndex: _selectedIndex,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Tab ONE'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Tab TWO'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _getPageAtIndex(_selectedIndex),
)
],
),
);
}
Widget _getPageAtIndex(int index) {
switch (index) {
case 0:
return _widgetC;
case 1:
return _widgetD;
}
return Container();
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
// ScrollController _scrollController;
#override
void initState() {
// _scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
// _scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
// controller: _scrollController,
child: SingleChildScrollView(
// controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}