Sticky tabs in NestedScrollView with ListViews - flutter

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(),
],
),

Related

How to make persistent AppBar and floating widget just below AppBar in flutter

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.

Flutter - how using SliverAppBar with Infinite Scroll Pagination?

I using Infinite Scroll Pagination plugin in my flutter's app. I need also using SilverAppBar in my page. This is my code:
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.call), text: "1"),
Tab(icon: Icon(Icons.message), text: "2"),
],
),
),
];
},
body: TabBarView(
children: [
const MyListWidget()
Text('2')
],
),
),
),
);
this is my MyListWidget:
Widget build(BuildContext context) {
return PagedSliverList<int, MyModel>(
pagingController: _сontroller,
builderDelegate: PagedChildBuilderDelegate<MyModel>(
itemBuilder: (context, item, index) {
return Text(item.Title);
},
),
);
}
But I have error:
A RenderRepaintBoundary expected a child of type RenderBox but received a child of type RenderSliverList.
Also I tried:
body: SliverFillRemaining(
child: TabBarView(
children: [
const ProfileSelections(),
//Container(child: Text('1')),
Text('2')
],
),
)
Than but I have error:
A RenderSliverFillRemainingWithScrollable expected a child of type RenderBox but received a child of type RenderSliverFillRemainingWithScrollable.
how can I fix these errors? any advice - I will be grateful
No need to use the Infinite scroll pagination, you can simply do with the flutter built-in scroll notification.
Scroll notification - abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin.
A Notification related to scrolling.
Scrollable widgets notify their ancestors about scrolling-related changes.
The notifications have the following lifecycle:
A ScrollStartNotification, which indicates that the widget has
started scrolling.
Zero or more ScrollUpdateNotifications, which indicate that the
widget has changed its scroll position, mixed with zero or more
OverscrollNotifications, which indicate that the widget has not
changed its scroll position because the change would have caused its
scroll position to go outside its scroll bounds.Interspersed with the
ScrollUpdateNotifications and
OverscrollNotifications are zero or more UserScrollNotifications,
which indicate that the user has changed the direction in which they
are scrolling.
A ScrollEndNotification, which indicates that the widget has stopped
scrolling.
A UserScrollNotification, with a UserScrollNotification.direction of
ScrollDirection.idle.
Here is the complete source code with explanations
import 'package:flutter/material.dart';
class InfiniteScrollPagination extends StatefulWidget {
const InfiniteScrollPagination({Key key}) : super(key: key);
#override
_InfiniteScrollPaginationState createState() =>
_InfiniteScrollPaginationState();
}
class _InfiniteScrollPaginationState extends State<InfiniteScrollPagination> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, value) {
return [
SliverAppBar(
pinned: true,
toolbarHeight: 0,
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.call), text: "1"),
Tab(icon: Icon(Icons.message), text: "2"),
],
),
),
];
},
body: TabBarView(
children: [MyListWidget(), Text('2')],
),
),
),
);
}
}
class MyListWidget extends StatefulWidget {
const MyListWidget({Key key}) : super(key: key);
#override
State<MyListWidget> createState() => _MyListWidgetState();
}
class _MyListWidgetState extends State<MyListWidget> {
int count = 15;
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
// here you update your data or load your data from network
setState(() {
count += 10;
});
}
return true;
},
// if you used network it would good to use the stream or future builder
child: Container(
child: getDataList(count),
),
);
}
}
getDataList(listOfData) {
return ListView.separated(
itemBuilder: (context, index) {
return ListTile(
title: Text("index $index"),
);
},
separatorBuilder: (context, index) => Divider(
thickness: 2,
color: Colors.grey,
),
itemCount: listOfData);
}
output:
it's happening because tabBarView needs normal box children rather than slivers because it uses pageview by default as you can read here in official documentation.
if you use normal list instead of slivers like below it will solve the problem:
Widget build(BuildContext context) {
return PagedListView<int, MyModel>(
pagingController: _сontroller,
builderDelegate: PagedChildBuilderDelegate<MyModel>(
itemBuilder: (context, item, index) {
return Text(item.Title);
},
),
);
}
Using PagedListView instead of PagedSliverList would solve the issue. Slivers are not widgets and Slivers are rendered in a different manner. we mostly use Slivers in CustomScrollView widget.

How to pin multiple SliverAppBars to the top from different scroll views

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.

Flutter: NestedScrollView Header TabBar obstructs/overlaps TabBarView Elements

My goal is to use some kind of Profile Page where I have a SliverAppBar at the top which has a tabBar as bottom to switch between the Lists or elements shown below the AppBar.
I found that this is archived by NestedScrollView but noticed a strange behavior when either having just a few elements and scrolling up, where it still scrolls up under the tab bar, even tho there are no new elements to show at the bottom, or when you have a lot of items and want to use some kind of persistent App Bar in one of the lists, to for example have a textfield which should be used to sort or filter the list.
I tried to use SliverOverlapAbsorber, but it didn't solve the problem. So below I just show the sample code without it:
I thought about inserting a Container that changes it's height according to the scrolling position, to 'solve' this issue, but I'd prefer the proper way to do this..
here's a gif to illustrate the problem:
https://gfycat.com/decentoccasionalhyena
https://gfycat.com/fastslightekaltadeta
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:widgettests/widgets/sliverHeader.dart';
class NestedTabbarSliverOverpaling extends StatefulWidget {
#override
_NestedTabbarSliverOverpalingState createState() => _NestedTabbarSliverOverpalingState();
}
class _NestedTabbarSliverOverpalingState extends State<NestedTabbarSliverOverpaling> with TickerProviderStateMixin {
ScrollController _scrollController;
TabController _tabController;
#override
void initState() {
super.initState();
_scrollController = new ScrollController();
_tabController = new TabController(length: 2, vsync: this);
}
#override
void dispose() {
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
controller: _scrollController,
physics: BouncingScrollPhysics(),
dragStartBehavior: DragStartBehavior.start,
headerSliverBuilder: (context, innerBoxIsScrolled) =>
<Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 100,
bottom: PreferredSize(
preferredSize: Size.fromHeight(-5),
child: TabBar(
controller: _tabController,
labelColor: Colors.white,
tabs: <Widget>[Tab(text: '1st List',), Tab(text: '2nd List',),]
),
),
),
],
body: TabBarView(
controller: _tabController,
children: <Widget>[
SafeArea(
top: false,
bottom: false,
child: _buildfirstList(context),
),
SafeArea(
top: false,
bottom: false,
child: _build2ndList(context),
),
],
),
),
);
}
Widget _buildfirstList(BuildContext context) {
final children = <Widget>[
CustomScrollView(
key: PageStorageKey<String>('1st'),
physics: ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
backgroundColor: Colors.blueGrey,
floating: true,
snap: true,
title: TextField(
showCursor: false,
),
titleSpacing: 2,
),
_List(),
//SliverFillRemaining(),
],
),
];
return Stack(
children: children,
);
}
Widget _build2ndList(BuildContext context) {
final children = <Widget>[
CustomScrollView(
key: PageStorageKey<String>('2nd'),
physics: ClampingScrollPhysics(),
slivers: <Widget>[
_List(),
//SliverFillRemaining(),
],
),
];
return Stack(
children: children,
);
}
Widget _List() {
return SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return new ListTile(
title: Text('tile no. ${index}'),
);
},
childCount: 55,
),
);
}
}

Flutter How to build a CustomScrollView with a non scrollable part

I want to have a view with on top a non scrollable part like an image for example with at the bottom a tab bar that i can scroll to the top to let appear a list of item and be able to scroll inside the list of item.
For that i used a CustomScrollView, with a sliver grid in place of the image for the moment, and a sliver app bar for the tabbar and a sliverFixedExtentList for the list.
Widget build(BuildContext context) {
return new Scaffold(
body: new CustomScrollView(
slivers: <Widget>[
new SliverGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.58,
crossAxisCount: 1,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
color: Colors.red,
child: new Container(
color: Colors.green,
child: new Text('IMG HERE'),
)
);
},
childCount: 1,
),
),
new SliverAppBar(
title: new Text("title"),
floating: false,
pinned: true,
primary: true,
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {
},
),
],
bottom: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: _bars,
),
),
new SliverFixedExtentList(
itemExtent: 100.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightGreen[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
)
);
}
But i have 3 problems :
I can't figure out how to make a sliver non scrollable for the slivergrid here.
I don't know how to make the appBar be placed exactly at the botom of the screen on launch.
I have a problem with the list when the appbar reach the top the list jump some items, it seems it represents the size of the sliverGrid element.
Thanks
I've tried your code and it seems that there are some missing essential parts there. I can't see what's the code behind _tabController and _bars, so I just made my own _tabController and _bars. I've run it and this is what I've got so far:
On launch:
Browsing till the AppBar goes to the top.
So I made some changes in your code for presentation purposes:
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(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
TabController _tabController;
List<Widget> _bars = [
Tab(icon: Icon(Icons.image)),
Tab(icon: Icon(Icons.image)),
];
int _selectedIndex = 0;
#override
void initState() {
super.initState();
_tabController = TabController(length: _bars.length, vsync: this);
_tabController.addListener(() {
setState(() {
_selectedIndex = _tabController.index;
});
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new CustomScrollView(
slivers: <Widget>[
new SliverGrid(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: .69,
crossAxisCount: 1,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SafeArea(
child: new Container(
color: Colors.green,
child: new Text('IMG HERE'),
),
);
},
childCount: 1,
),
),
new SliverAppBar(
title: new Text("title"),
floating: false,
pinned: true,
primary: true,
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {},
),
],
bottom: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: _bars,
),
),
new SliverFixedExtentList(
itemExtent: 100.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightGreen[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
));
}
}
Here is the output:
As you can see, I've played around with the value of childAspectRatio so that you can set the AppBar` at the bottom of the screen by default, that's how I understood your question number 2.
For question number 3, it seems that your code is working fine. I am able to properly see the ascending list item from 0 sequenced properly.
And for question number 1, I am quiet confused of how you want it to happen. You don't want the SliverGrid to be scrollable but you are expecting the AppBar to be on the top of the screen after scrolling. I guess giving more context on this part could give clarity for everyone.