How to add Grids into TabbarView in Flutter? - flutter

So basically I have this widget:
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// ...
SliverToBoxAdapter(
child: TabBar(
controller: this._controller,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(width: 1.0, color: Colors.red),
),
tabs: [
Tab(icon: Icon(CupertinoIcons.camera)),
Tab(icon: Icon(CupertinoIcons.photo)),
Tab(icon: Icon(CupertinoIcons.video_camera)),
],
),
),
SliverFillRemaining(
child: TabBarView(
controller: this._controller,
children: [
// Want Scrollable Grid here
// Want Scrollable Grid here
Center(
child: Text("Hello Reader🙂"),
),
],
),
),
// ...
],
),
);
}
}
I want to add a 2 scrollable grids as children in the TabBarView however when I use GridView.builder(...), there is an annoying gap at the top of the grid and scrolling isn't all too great neither even with shrinkWrap: true and physics: NeverScrollableScrollPhysics().
However when I use a SliverGrid(...), there is this error
RenderObjects expect specific types of children because they coordinate with their children during layout and paint. For example, a RenderSliver cannot be the child of a RenderBox because a RenderSliver does not understand the RenderBox layout protocol.
This obviously makes sense because TabBarView isn't a sliver widget. I have already taken a look at this post but it wasn't really of any help.
How could I implement this? Is there perhaps a way I could create my own widget builder that builds a custom layout?
Thank You!

You need to use SliverOverlapAbsorber/SliverOverlapInjector, the following code works for me (working full code on dart pad):
Here i used SliverFixedExtentList but you can it replace with SliverGrid.
#override
State<StatefulWidget> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
final List<String> listItems = [];
final List<String> _tabs = <String>[
"Featured",
"Popular",
"Latest",
];
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
sliver: SliverAppBar(
title: const Text('Books'),
floating: true,
pinned: true,
snap: false,
primary: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is "inside"
// the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
// find the NestedScrollView.
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),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 60.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return Container(
color: Color((math.Random().nextDouble() *
0xFFFFFF)
.toInt() <<
0)
.withOpacity(1.0));
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
),
);
}
}

Related

PageView inside SliverChildBuilderDelegate starting at last page when scrolling fast

I needed a floating SliverAppBar with TabBarView. Each tab has a CustomScrollView to scroll it's Childs. If i put PageView as a child and i scroll fast then the PageView starts with last page instead of first page. I found a sample code here in flutter docs:
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
And i just changed the ListTile inside the SliverChildBuilderDelegate with a PageView. But when i test this code and scroll fast the pageView starts with last page.
Here's a demo of that
https://gyazo.com/79709286236fa3dbf1f88b1e92f5cee3
And here's my code:
// This example shows a [NestedScrollView] whose header is the combination of a
// [TabBar] in a [SliverAppBar] and whose body is a [TabBarView]. It uses a
// [SliverOverlapAbsorber]/[SliverOverlapInjector] pair to make the inner lists
// align correctly, and it uses [SafeArea] to avoid any horizontal disturbances
// (e.g. the "notch" on iOS when the phone is horizontal). In addition,
// [PageStorageKey]s are used to remember the scroll position of each tab's
// list.
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: MyStatelessWidget(),
);
}
}
/// This is the stateless widget that the main application instantiates.
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
return DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title:
const Text('Books'), // This is the title in the app bar.
pinned: true,
floating: true,
expandedHeight: 150.0,
// The "forceElevated" property causes the SliverAppBar to show
// a shadow. The "innerBoxIsScrolled" parameter is true when the
// inner scroll view is scrolled beyond its "zero" point, i.e.
// when it appears to be scrolled below the SliverAppBar.
// Without this, there are cases where the shadow would appear
// or not appear inappropriately, because the SliverAppBar is
// not actually aware of the precise position of the inner
// scroll views.
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
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),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return PageView(
/// [PageView.scrollDirection] defaults to [Axis.horizontal].
/// Use [Axis.vertical] to scroll vertically.
scrollDirection: Axis.horizontal,
children: const <Widget>[
Center(
child: Text('First Page'),
),
Center(
child: Text('Second Page'),
),
Center(
child: Text('Third Page'),
)
],
);
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 100,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
How can i make the pageView to starts with first page? I tried setting this to pageView controller: PageController(initialPage: 0), But it didn't work.

Refresh Indicator not working with NestedScrollView

#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()
],
),
),
],
),
),),
),
}
Refresh Indicator not working with NestedScrollView, is there any way to implement RefreshIndiactor?
I tried adding an empty ListView in Stack, Refresh indicator start showing but because of that my NestedScrollView doesnt scroll.
Did you try setting NestedScrollView widgets physics to AlwaysScrollableScrollPhysics()?
Please see the documentation for RefreshIndicator.
Refresh indicator does not show up
The RefreshIndicator will appear if its scrollable descendant can be
overscrolled, i.e. if the scrollable's content is bigger than its
viewport. To ensure that the RefreshIndicator will always appear, even
if the scrollable's content fits within its viewport, set the
scrollable's Scrollable.physics property to
AlwaysScrollableScrollPhysics:
ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: ... )
A RefreshIndicator can only be used with a vertical scroll view.
Please try to wrap 'body' of the NestedScrollView with RefreshIndicator widget if the headers won't change when refreshed but only the content change happens for 'body'.

Auto scroll to one of SliverList items when a Tab is pressed

I have a stateful widget with tabs at the top and has a list below it. The list is subdivided into different categories. Each category is listed into each tab. What I want to do is when an item is pressed in the tab, I want its corresponding category in the list to scroll to view.
Here is the relevant code for reference:
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarTabDelegate(
child: Container(
child: TabBar(
isScrollable: true,
labelColor: Colors.black,
tabs: createTabList(),
controller: tabBarController,
),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.only(top: kSpacerRegular),
child: RestaurantMenuCard(menuCard: menuCards[index]),
);
},
childCount: menuCards.length,
),
),
I searched all day looking for a solution but could not find one. I also tried this package but does not seem to work in my case.
Here is my 'widget tree' for more context:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(),
SliverPersistentHeader(),
SliverList(),
],
),
);
}
A screen capture for more context
I solved the issue with the help of this answer. I replaced SliverList with SliverToBoxAdapter. The child is the list of my RestaurantMenuCard in a Column. Thus, the resulting widget tree is:
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(),
SliverPersistentHeader(),
SliverToBoxAdapter(
Column(
RestaurantMenuCard(),
RestaurantMenuCard(),
...
),
),
],
),
);
}
The magic happens when I added a global key to each instance of the RestaurantMenuCard as discussed in the linked answer.
Then finally, I trigger the Scrollable.ensureVisible(context) in onTap function of my TabBar.

NestedScrollView body keeps scrolling even if empty space is available

My requirement is to create a page with collapsing toolbar and two tabs.
To do this, I am using the code below.
SafeArea(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverPersistentHeader(
pinned: true,
delegate: _Header(),
)
];
},
body: TabBarView(
children: <Widget>[
Container1(),
Container2(),
],
),
),
),
)
Everything works, but once the scrolling starts and the header collapses, the body keeps on scrolling even if all the widgets are visible.
How do I make it behave like Android native, where if the list is small in size, the list doesn't scroll after the header collapses.
For anyone who is facing the same issue, this is how I fixed it.
You can just copy the entire code and run it to check and modify accordingly.
The code is taken from the official documentation.
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_module/app_constants.dart';
import 'package:flutter_module/data/local/shared_pref.dart';
import 'package:flutter_module/data/remote/api_end_points.dart';
import 'package:flutter_module/string_localization.dart';
import 'package:flutter_module/ui/router.dart';
import 'package:flutter_module/util.dart';
import 'package:http/http.dart' as http;
import 'data/local/dao/sync_dao.dart';
import 'data/remote/response_pojo/video_challenges_response.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MainCollapsingToolbar(),
);
}
}
class MainCollapsingToolbar extends StatefulWidget {
#override
_MainCollapsingToolbarState createState() => _MainCollapsingToolbarState();
}
class _MainCollapsingToolbarState extends State<MainCollapsingToolbar> {
#override
Widget build(BuildContext context) {
var _tabs = ["One", "Two"];
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(minHeight: 100, maxHeight: 150, child: Container(
color: Colors.blue,
)),
),
),
];
},
body: TabBarView(
// These are the contents of the tab views, below the tabs.
children: _tabs.map((String name) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
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),
// In this example, the inner scroll view has
// fixed-height list items, hence the use of
// SliverFixedExtentList. However, one could use any
// sliver widget here, e.g. SliverList or SliverGrid.
sliver: SliverFixedExtentList(
// The items in this example are fixed to 48 pixels
// high. This matches the Material Design spec for
// ListTile widgets.
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// This builder is called for each child.
// In this example, we just number each list item.
return ListTile(
title: Text('Item $index'),
);
},
// The childCount of the SliverChildBuilderDelegate
// specifies how many children this inner list
// has. In this example, each tab has a list of
// exactly 30 items, but this is arbitrary.
childCount: 1,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
wrap your body with Expanded widget

Using a NestedScrollView and supplying a ScrollController downstream to a ListView

I have a NestedScrollView which works well to AutoHide the AppBar (one feature I want) when I use SliverAppBar. Where I am running into problems, is I use ListView.Builder as one of the body components downstream that I need apply its own ScrollController to (or seems I need to apply it here). This conflicts with the NestedScrollView and I lose the autohide of the appbar that is conveniently handled by the NestedScrollView and SliverAppBar.
If I attach the ScrollController on the NestedScrollView Then it only tracks scroll position up to an offset of 80.0 and after that, with a longer ListView I am unable to properly animateTo as I can with the ScrollController attached directly to the ListView.Builder.
Here is a snippet/sudo code of my implementation:
new Scaffold(
drawer: ...,
body: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
new SliverAppBar(
title: new Text('Title'),
floating: true,
snap: true
)
]
}
body: new Stack(
children: <Widget>[
new PageView(
children: <Widget>[
new PageView1(implements ListViewBuilder),
new PageView2(implements ListView),
new PageView3(implements ListView),
]
controller: _pageController,
),
new FloatingActionButton
]
)
)
)
class PageView1 extends StateFulWidget {
...//Builder return scrollable with max offset of 2000.0
return new ListView.builder(
itemBuilder: itemBuilder,
itemCount: objects.length,
controller: _scrollController,
);
...
#override
void initState{
scrollController = new scrollController();
scrollController.animateTo(800.0, ....);
}
}
The nice part about this is the PageView2 and 3 behave nicely, with the autohide of the app bar on scroll behavior as I am not creating ScrollControllers there. But, PageView1 behaves incorrectly and autoHide of the appbar breaks. But, I really want to be able to animateTo correctly and am unable to do so without placing the controller directly on the ListViewBuilder.
Any thoughts on a better implementation that would help me achieve this?
UPDATE: I have updated my implementation to more closely follow the NestedScrollView documentation. But, with no luck. It seems the addition of the ScrollController on the NestedScrollView only tracks the position of the SliverAppBar, and ScrollController.jumpTo or animateTo only jump to a maximum of the AppBar (offset 80)
I worked it out.. This is not how I expected it to work at all. I moved my SliverList into the headerSliverBuilder and it works the way I want it to. I took the cue to do so from this NestedScrollView example gist: https://gist.github.com/collinjackson/2bc6697d31e6b94ada330ef5e818a36f
Follow the NestedScrollViewExample:
Change your list view to SliverList or SliverFixedExtentList and Wrap it inside a safe area and a CustomScrollView:
return SafeArea(
top: false,
bottom: false,
child: Builder(builder: (BuildContext context) => CustomScrollView(
slivers: <Widget>[
return SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) => ChildWidget(items[i]),
childCount: items.length,
),
),
],
)),
);
I met with the same problem and here is my solution. You can add a key to NestedScrollView and use it to access the inner CustomScrollView/ListViewcontroller of the tab, you only need to add the ScrollController to NestedScrollView
final GlobalKey<NestedScrollViewState> documentsNestedKey = GlobalKey();
void initState() {
_scrollController = ScrollController();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(() {
setState(() {});
});
// Tabs Pagination
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
documentsNestedKey.currentState!.innerController.addListener(() {
if (documentsNestedKey.currentState!.innerController.positions.last.atEdge) {
if (documentsNestedKey.currentState!.innerController.positions.last.pixels != 0) {
if (_tabController.index == 0) {
context.read<DeclarationsBloc>().add(const FetchDeclarationsEvent());
} else {
context.read<TasksBloc>().add(const FetchTasksEvent());
}
}
}
});
});
super.initState();
}
return SafeArea(
child: NestedScrollView(
key: documentsNestedKey,
controller: _scrollController,
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
backgroundColor: theme.background,
floating: true,
pinned: true,
forceElevated: innerBoxIsScrolled,
flexibleSpace: FlexibleSpaceBar(
background: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 11),
Padding(
padding: responsiveUtil.getHorizontalPadding(),
child: SearchWithFilter(
searchController: widget.searchController,
currentMenuIndex: 3,
onSearch: (text) {},
onSearchClear: () {},
onFilterTap: widget.onFilterTap,
),
),
],
),
),
bottom: DeclarationsTabBar(
tabController: _tabController,
),
),
),
],
body: TabBarView(
controller: _tabController,
children: [
MyDeclarationsTab(searchController: widget.searchController),
TasksTab(searchController: widget.searchController),
],
),
),
);