I'm trying to use a NestedScrollView for scrollable tabs within a DraggableScrollableSheet and need to pass the sheet's controller to the NestedScrollView's scrollable body, however doing so is not supported by NestedScrollView which requires that inner scrollables implicitly use the context's PrimaryScrollController.
The resultant behaviour is that I am seeing:
The draggable sheet behaviour is correct and the sheet moves up before scrolling but the header's elevation is not applied once scrolling nor is the header responding to drag events ("Foo" tab below), OR;
The inverse of the above - the sheet does not respond to drag events but the header's elevation is correctly applied and the header itself correctly responds to gestures for scrolling ("Bar" tab below)
I've tried a multiple combinations of NestedScrollView, CustomScrollView, and TabView, tried adding the controller to all, some, none of the scrollables. No luck. I have even tried listening to Notification events from either attached scrollable and update the other one with those events to try to keep them in sync. Nada.
Would appreciate any and all help on this, I've been tearing my hair out for a while now. Thank you!
Here's the code for main.dart in the video above:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: DraggableScrollableSheet(
initialChildSize: 0.6,
maxChildSize: 0.9,
minChildSize: 0.6,
builder: (context, scrollController) => DefaultTabController(
length: 2,
child: _buildNestedScrollView(scrollController),
),
),
);
}
NestedScrollView _buildNestedScrollView(ScrollController scrollController) {
return NestedScrollView(
// controller: scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) {
print('innerBoxIsScrolled $innerBoxIsScrolled');
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
toolbarHeight: 0.0,
pinned: true,
primary: false,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: [
Tab(text: 'Foo'),
Tab(text: 'Bar'),
],
),
),
),
];
},
body: TabBarView(
children: [
ListView.builder(
controller: scrollController,
physics: ClampingScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) => Container(
color: Colors.primaries[index % 10],
height: 150.0,
),
),
ListView.builder(
// controller: scrollController,
physics: ClampingScrollPhysics(),
padding: EdgeInsets.zero,
itemBuilder: (context, index) => Container(
color: Colors.primaries[index % 10],
height: 25.0,
),
),
],
),
);
}
}
Relevant links:
https://api.flutter.dev/flutter/widgets/ScrollableWidgetBuilder.html
https://api.flutter.dev/flutter/widgets/NestedScrollView/body.html
Potentially relevant Flutter issue.
Related
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.
#override
Widget build(BuildContext context) {
super.build(context);
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
),
child: Scaffold(
key: _scaffoldKeyProfilePage,
body: DefaultTabController(
length: 2,
child:RefreshIndicator(
onRefresh: _onRefresh,
child: NestedScrollView(
headerSliverBuilder: (context, _) {
return [
SliverList(
delegate: SliverChildListDelegate(
[ BuildMainProfile(
....//
),
Padding(
...//another design
),
];
},
// You tab view goes here
body: Column(
children: <Widget>[
TabBar(
tabs: [
Tab(text: 'A'),
Tab(text: 'B'),
],
),
Expanded(
child: TabBarView(
children: [
BuildPost(,
),
BuildWings()
],
),
),
],
),
),),
),
}
Above is the example of error which I am getting
error:A RenderFlex overflowed by 48 pixels on the bottom.
How to solve this issue? Tried using expanded on TabBar and giving flex of 1 to tab bar and flex of 10 to tabView , but with that tab bar shrinks on scrolling down.
Here below is the code for tabBar view A and B is even similar
class BuildPost extends StatefulWidget {
final String uid;
const BuildPost({
Key key,
#required this.uid,
}) : super(key: key);
#override
_BuildPostState createState() => _BuildPostState();
}
class _BuildPostState extends State<BuildPost> {
List<Post> _post = [];
getUsersPost() async {
final database = FirestoreDatabase();
List<Post> _postModel = await database.getUsersPost(widget.uid);
setState(() {
_post = _postModel.toList();
});
}
#override
void initState() {
getUsersPost();
super.initState();
}
#override
Widget build(BuildContext context) {
return _post.isEmpty
? Container(
height: 500,
width: double.infinity,
)
: GestureDetector(
child: Directionality(
textDirection: TextDirection.ltr,
child: AnimationLimiter(
child: StaggeredGridView.countBuilder(
padding: EdgeInsets.all(10),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 3,
itemCount: _post.length,
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredGrid(
position: index,
duration: const Duration(milliseconds: 500),
columnCount: 3,
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
duration: Duration(milliseconds: 1000),
child: BuildData(
totalPost: _post.length,
postList: _post,
index: index,
post: _post[index],
)),
),
);
},
staggeredTileBuilder: (index) => StaggeredTile.count(
index % 7 == 0 ? 2 : 1,
index % 7 == 0 ? (2.1) : (1.05)),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
),
)),
);
}
}
It is because the body height of NestedScrollView is from 0 to MediaQuery.of(context).size.height, while your TabBar inside the column make it layout a minimal height of TabBar.
Move TabBar inside builder
Form the example of NestedScrollView, you can see the TabBar is inside headerSliverBuilder. You can simply move the TabBar inside it (wrap a SliverToBoxAdapteror SliverAppBar to make it sliver).
Then you can remove the Column and Expand Widget above the TabBarView
child: NestedScrollView(
headerSliverBuilder: (context, _) {
return [
SliverList(
...
),
SliverAppBar(
pinned: true,
primary: false, // no reserve space for status bar
toolbarHeight: 0, // title height = 0
bottom: TabBar(
tabs: [
Tab(text: 'A'),
Tab(text: 'B'),
],
),
)
];
}
body: TabBarView(
children: [
...
The body property of NestedScrollView gets a tight height constraint equal to the space left over by headerSliverBuilder (considering the scroll position). In your code, you've as body a Column widget with a fixed height (the TabBar) widget in there. So when the height constraint of body gets smaller than the TabBar height, it will overflow the Column.
So in body, there must be a widget that can shrink to zero height, most likely a scrollable (ListView, CustomScrollView). In your case, you can move the TabBar to the bottom of headerSliverBuilder, wrapping it with:
SliverPersistentHeader(
pinned: true,
delegate: SimpleHeaderDelegate(
child: TabBar(...),
),
)
using:
class SimpleHeaderDelegate extends SliverPersistentHeaderDelegate {
SimpleHeaderDelegate({#required this.child});
final PreferredSizeWidget child;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
#override
double get maxExtent => child.preferredSize.height;
#override
double get minExtent => child.preferredSize.height;
#override
bool shouldRebuild(covariant SimpleHeaderDelegate oldDelegate) => oldDelegate.child != child;
}
See SingleChildScrollView class, Expanding content to fit the viewport:
https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html
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,
),
);
}
}
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(),
],
),
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.