I want to use DefaultTabController in the middle of the screen along with other widgets. There will be a widget before the TabBar. I managed to find an answer for this in SO. But the problem is I want to scroll the entire page. Provided answer only scroll the tab section. I added SingleChildScrollView by wrapping the entire thing. But that didn't work as that conflict with the Expanded widget.
Then I found another answer which uses CustomScrollView with slivers but again that only scroll the tab section not the entire screen.
Erroneous code with SingleChildScrollView: https://dartpad.dev/d380be0134d0c4e84b5dca87a58b3022
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 150, child: Text('Hey')),
Expanded(child: ProfileTabBarNavigation()),
],
),
),
);
}
}
const String kArtwork = "Left";
const String kPastJobs = "Right";
const EdgeInsets kPaddingTabBar = EdgeInsets.all(5.0);
const Color kLightGrey = Colors.grey;
class ProfileTabBarNavigation extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
const Tab(text: kArtwork),
const Tab(text: kPastJobs)
];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
appBar: TabBar(
tabs: myTabs,
unselectedLabelColor: Colors.black54,
labelColor: Colors.black,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(50),
color: Colors.white,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text(label),
);
},
);
}).toList(),
),
),
);
}
}
CustomScrollView: https://dartpad.dev/335f9e0d134a72232200bc8bfd493670
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: true,
child: Column(
children: [
SizedBox(height: 150, child: Text('Hey')),
Expanded(child: ProfileTabBarNavigation()),
],
),
)
],
),
);
}
}
const String kArtwork = "Left";
const String kPastJobs = "Right";
const EdgeInsets kPaddingTabBar = EdgeInsets.all(5.0);
const Color kLightGrey = Colors.grey;
class ProfileTabBarNavigation extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
const Tab(text: kArtwork),
const Tab(text: kPastJobs)
];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
appBar: TabBar(
tabs: myTabs,
unselectedLabelColor: Colors.black54,
labelColor: Colors.black,
indicatorSize: TabBarIndicatorSize.tab,
),
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return ListView.builder(
// physics: NeverScrollableScrollPhysics(),
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text(label),
);
},
);
}).toList(),
),
),
);
}
}
Got the CustomScrollView solution from here: How to use Expanded in SingleChildScrollView?
Short answer: you need to use NestedScrollView. You also need to adjust a few things in your current code, so longer answer:
const String kArtwork = "Left";
const String kPastJobs = "Right";
const EdgeInsets kPaddingTabBar = EdgeInsets.all(5.0);
const Color kLightGrey = Colors.grey;
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
final String title = 'Multi Sliver Scrollable';
return MaterialApp(
title: title,
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: Home(title),
);
}
}
class Home extends StatelessWidget {
Home(this.title);
final String title;
final List<Tab> myTabs = <Tab>[
const Tab(text: kArtwork),
const Tab(text: kPastJobs)
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: DefaultTabController(
length: myTabs.length,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SizedBox(height: 150, child: Text('Hey'))),
SliverAppBar(
toolbarHeight: 0,
bottom: TabBar(
tabs: myTabs,
indicatorSize: TabBarIndicatorSize.tab,
),
),
];
},
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return CustomScrollView(
key: PageStorageKey<String>(label),
slivers: [
SliverList(
delegate: SliverChildListDelegate.fixed(
List.filled(50, Text(label)),
),
),
],
);
}).toList(),
),
),
),
);
}
}
Related
I want to make my AppBar persistent (it should not float like tabbar) while I want to place a widget or say tabbar just below appbar which will float and be pinned to appbar just like spotify app of library page see below for proper understanding
So far I have used slivers in flutter not able to achieve what I want,
Please check my code and correct me,
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Test()),
);
}
}
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(excludeHeaderSemantics: true,
floating: true,
pinned: true,
title: Text('This should be fixed not moving'),
bottom: TabBar(
tabs: [
Tab( text: "Call"), // this shoudl be floating n pinned
Tab( text: "Message"),// this should be floating n pinned
],
),
),
];
},
body: TabBarView(
children: [
Container(child: ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Text("Item $index");
})),
Container(child: ListView.builder(
itemCount: 100,
itemBuilder: (context,index){
return Text("Item $index");
})),
],
),
),
),
);
}
}
You should use SliverList after the SliverAppBar and put your TabBar inside it.
here's what you are looking for.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Test()),
);
}
}
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
excludeHeaderSemantics: true,
floating: true,
pinned: true,
title: Text('This should be fixed not moving'),
),
SliverList(
delegate: SliverChildListDelegate.fixed(
[
TabBar(
labelColor: Colors.blue,
tabs: [
Tab(text: "Call"),
Tab(text: "Message"),
],
),
],
),
),
];
},
body: TabBarView(
children: [
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text("Item $index");
})),
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text("Item $index");
})),
],
),
),
),
);
}
}
Code below is working
as I wanted,
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Test()),
);
}
}
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
floating: true,
pinned: true,
title: Text('This should be fixed not moving'),
),
SliverList(
delegate: SliverChildListDelegate.fixed(
[
TabBar(
labelColor: Colors.blue,
tabs: [
Tab(text: "Call"),
Tab(text: "Message"),
],
),
],
),
),
];
},
body: TabBarView(
children: [
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text("Item $index");
})),
Container(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text("Item $index");
})),
],
),
),
),
);
}
}
You can use the bottom attribute for that on the AppBar.
Here is a link to an example on how to do it:
https://esflutter.dev/docs/catalog/samples/app-bar-bottom
You could also restructure your UI to make it like this:
Scaffold(
appBar: <HERE_YOUR_APPBAR>,
body: Column(
children: [
TabBar(), // here your tab bar,
Expanded(
child: TabBarView() // here your tabs
)
]
)
)
This way your TabBar will be anchored to the top while the Expanded will cover the rest of the Column's real estate and fill your page.
Here is the case, the first sliverAppBar pins to the top correctly when scrolling. Within customScrollView are two tabs that have its' own scroll view. Inside the first tab, is another sliverAppBar that is supposed to pin under the first one. However, it slides beneath the first sliverAppBar. Now since our complex view does not allow only one customScrollView to fix the problem, is there any other way?
See image here (Notice how first AppBar overlaps the second AppBar)
Desired Effect Before Scroll
Desired Effect After Scroll
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Test(),
);
}
}
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final List<String> _tabs = ['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
title: const Text('1st App Bar'),
pinned: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
];
},
body: TabBarView(
children: [
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 3,
),
),
),
const SliverAppBar(
toolbarHeight: 150,
pinned: true,
backgroundColor: Colors.purple,
title: Text('2nd App Bar'),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 20,
),
),
),
],
),
Placeholder(),
],
),
),
),
);
}
}
We can use Scaffold to hold 1st appBar. And inside tab we can make 2nd appBar as pinned. Let me know if you wish any changes.
Output
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> with SingleTickerProviderStateMixin {
static const List<Tab> myTabs = <Tab>[
Tab(text: 'Tab1'),
Tab(text: 'Tab2'),
];
late TabController controller;
#override
void initState() {
super.initState();
controller = TabController(length: myTabs.length, vsync: this);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Appbar 1"),
bottom: TabBar(
controller: controller,
tabs: myTabs,
),
),
body: TabBarView(
controller: controller,
children: [
CustomScrollView(
slivers: [
SliverAppBar(
title: Text("AppBar 2"),
pinned: true,
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 213,
),
),
),
],
),
Container(
color: Colors.pink,
),
],
),
);
}
}
I think u can use SliverPersistentHeader and SliverAppBar.
Iam trying to implement a appbar like this
When scrolling down I need to hide the search bar alone and pin the row and the tabs on the device top. Which is like
And when we scroll down the all the three rows needs to be displayed.
Using SliverAppBar with bottom property tabs are placed and pinned when scrolling, but a row above it should be pinned at the top above the tabbar. Im not able to add a column with the row and tabbar because of preferedSizeWidget in bottom property. Flexible space bar also hides with the appbar so I cannot use it. Does anyone know how to make this layout in flutter.
Please try this.
body: Container(
child: Column(
children: <Widget>[
Container(
// Here will be your AppBar/Any Widget.
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
// All your scroll views
Container(),
Container(),
],
),
),
),
],
),
),
You could create your own SliverAppBar or you can divide them in 2 items, a SliverAppBar and a SliverPersistentHeader
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home>
with SingleTickerProviderStateMixin {
TabController controller;
TextEditingController textController = TextEditingController();
#override
void initState() {
super.initState();
controller = TabController(
length: 3,
vsync: this,
);
}
#override
void dispose(){
super.dispose();
controller.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(color: Colors.black.withOpacity(.5), fontSize: 16),
border: InputBorder.none
)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
//This is Where you create the row and your tabBar
SliverPersistentHeader(
delegate: MyHeader(
top: Row(
children: [
for(int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
)
)
]
),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
controller: controller,
),
),
pinned: true,
),
SliverFillRemaining(
child: TabBarView(
controller: controller,
children: <Widget>[
Center(child: Text("Tab one")),
Center(child: Text("Tab two")),
Center(child: Text("Tab three")),
],
),
),
],
),
);
}
}
//Your class should extend SliverPersistentHeaderDelegate to use
class MyHeader extends SliverPersistentHeaderDelegate {
final TabBar bottom;
final Widget top;
MyHeader({this.bottom, this.top});
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).accentColor,
height: math.max(minExtent, maxExtent - shrinkOffset),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if(top != null)
SizedBox(
height: kToolbarHeight,
child: top
),
if(bottom != null)
bottom
]
)
);
}
/*
kToolbarHeight = 56.0, you override the max and min extent with the height of a
normal toolBar plus the height of the tabBar.preferredSize
so you can fit your row and your tabBar, you give them the same value so it
shouldn't shrink when scrolling
*/
#override
double get maxExtent => kToolbarHeight + bottom.preferredSize.height;
#override
double get minExtent => kToolbarHeight + bottom.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}
UPDATE
A NestedScollView let you have 2 ScrollViews so you can control the inner scroll with the outer (just like you want with a TabBar)
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController textController = TextEditingController();
List<String> _tabs = ['Tab 1', 'Tab 2', 'Tab 3'];
// Your tabs, or you can ignore this and build your list
// on TabBar and the TabView like my previous example.
// I don't create a TabController now because I wrap the whole widget with a DefaultTabController
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
elevation: 0.0,
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(
color: Colors.black.withOpacity(.5),
fontSize: 16),
border: InputBorder.none)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
),
SliverPersistentHeader(
delegate: MyHeader(
top: Row(children: [
for (int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
))
]),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
pinned: true,
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
// You can ignore it if you're going to build your
// widgets in another Stateless/Stateful class.
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber
// above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
onTap: () => print('$name at index $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
));
}
}
import 'dart:io';
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(
primaryColor: Colors.white,
),
home: NewsScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class NewsScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
final List<String> _tabs = <String>[
"Featured",
"Popular",
"Latest",
];
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
bottom: Platform.isIOS ? false : true,
sliver: SliverAppBar(
title: Text('Tab Demo'),
elevation: 0.0,
floating: true,
pinned: true,
snap: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
),
];
},
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
),
);
}
}
I have TabBarView with 3 children, one of them I have a container which has an onPanUpdate gesture.
When I am trying to move container tab bar onHorizontalDragUpdate calls and tab bar changes tab but not container.
Is there any way to prevent onHorizontalDragUpdate if I taped on a container like stoppropagation in javascript?
TabBarView(
controller: _tabController,
children: [
Container(
color: Colors.red,
),
TabWithDragableContainer(),
Container(
color: Colors.blue,
),
],
)
You could disable the gestures for all the tabs by specifying physics: NeverScrollableScrollPhysics() inside TabBarView.
Minimal Working Example
This example has three tabs:
Basic Tab
Tab with Draggable Widget
Tab with onPan GestureDetector
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
),
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: [
Icon(Icons.directions_car),
_ContainerWithDraggable(),
_ContainerWithPanGesture(),
],
),
),
);
}
}
class _ContainerWithDraggable extends HookWidget {
#override
Widget build(BuildContext context) {
final _offset = useState(Offset(100, 100));
return Stack(
children: [
Container(color: Colors.amber.shade100),
Positioned(
top: _offset.value.dy,
left: _offset.value.dx,
child: Draggable(
onDragUpdate: (details) => _offset.value += details.delta,
feedback: _Circle(),
child: _Circle(),
),
),
],
);
}
}
class _ContainerWithPanGesture extends HookWidget {
#override
Widget build(BuildContext context) {
final _offset = useState(Offset(100, 100));
final _previousOffset = useState<Offset>(null);
final _referenceOffset = useState<Offset>(null);
return Stack(
children: [
GestureDetector(
onPanStart: (details) {
_previousOffset.value = _offset.value;
_referenceOffset.value = details.localPosition;
},
onPanUpdate: (details) => _offset.value = _previousOffset.value +
details.localPosition -
_referenceOffset.value,
child: Container(color: Colors.amber.shade100),
),
Positioned(
top: _offset.value.dy,
left: _offset.value.dx,
child: _Circle(),
),
],
);
}
}
class _Circle extends StatelessWidget {
const _Circle({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.brown, width: 5.0),
),
);
}
}
The layout works as desired, except this:
When I scroll on one page, the second page scrolls too. Not as much but enough to obscure the first item.
I could imagine it'd have something to do with the NestedScrollView but I don't know how to go on.
import 'package:flutter/material.dart';
main(){
runApp(new MaterialApp(
home: new MyHomePage(),
));
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new DefaultTabController(
length: 2,
child: new Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
title: const Text('Tabs and scrolling'),
forceElevated: innerBoxIsScrolled,
pinned: true,
floating: true,
bottom: new TabBar(
tabs: <Tab>[
new Tab(text: 'Page 1'),
new Tab(text: 'Page 2'),
],
),
),
];
},
body: new TabBarView(
children: <Widget>[
_list(),
_list(),
],
),
),
),
);
}
Widget _list(){
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: 250,
itemBuilder: (context, index){
return Container(
color: Colors.grey[200].withOpacity((index % 2).toDouble()),
child: ListTile(
title: Text(index.toString()),
),
);
}
);
}
}
To be able to keep the two ListViews to scroll without affecting each other they need to have defined controllers.
To have the ListViews maintain their scroll position between tab switching you need to have them in a Widget with AutomaticKeepAliveClientMixin.
Here's an example of what you can do instead of your _list method. Defined a Stateful Widget that returns your lists using both controllers and the AutomaticKeepAliveClientMixin:
class ItemList extends StatefulWidget {
#override
_ItemListState createState() => _ItemListState();
}
class _ItemListState extends State<ItemList> with AutomaticKeepAliveClientMixin{
ScrollController _scrollController = ScrollController();
#override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero,
itemCount: 250,
itemBuilder: (context, index){
return Container(
color: Colors.grey[200].withOpacity((index % 2).toDouble()),
child: ListTile(
title: Text(index.toString()),
),
);
}
);
}
#override
bool get wantKeepAlive => true;
}
You can just call it normally like any other widget inside your TabBarView:
TabBarView(
children: <Widget>[
ItemList(),
ItemList(),
],
),