Flutter docs are very confusing, what's the use of SliverOverlapAbsorber and SliverOverlapInjector? - flutter

I am using this example from flutter official docs. Here is the minimal code:
List<String> _tabs = ["Tab1", "Tab2"];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
// 1/2 remove this widget and only use SliverAppBar
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverAppBar(
title: const Text('Books'),
pinned: true,
expandedHeight: 150.0,
bottom: TabBar(tabs: _tabs.map((name) => Tab(text: name)).toList()),
),
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(name),
slivers: <Widget>[
// 2/2 remove this widget
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(_, i) => ListTile(title: Text('Item $i')),
childCount: 30,
),
)
],
);
},
);
}).toList(),
),
),
),
);
}
Problem:
As you can see if I remove SliverOverlapAbsorber along with SliverOverlapInjector in above code, I don't see any change in the output, what's the use of having it? Docs say
SliverOverlapInjector: 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.
Can anyone explain what does it mean and what's the use of having SliverOverlapInjector and SliverOverlapInjector

I use it in NestedScrollView
NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar()
],
body: ...
)
Without it everything in your body will go under appbar.
Check out https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html
It's said there
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),

Related

How to add Grids into TabbarView in 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(),
),
),
),
),
);
}
}

NestedScrollView No Scroll when List is Short with Float SliverAppBar

I am having a ListView, that is required to use pull to refresh function, and a floating header is needed on the design. Therefore I have tried to use the NestedScrollView to fulfill the task. However, I find that even if the list is short, as the SliverAppBar is floating, the body is always a bit longer to make the list able to scroll to hide the app bar which is not what expected on the our design flow.
The design flow considered is to
pin the appbar (not able to scroll) when the body is shorter than the available space
able to float app bar when list body is longer than the available space
I have tried to listen the scroll notification, but it seems not work as what i want to obtain. Please give me some suggestions if there is any way can solve this problem. Thank you!
The following is the code so far i have tried:
return Scaffold(
body: Builder(
builder: (context, ) {
return SafeArea(
child: NotificationListener<ScrollNotification>(
onNotification: (info){
debugPrint('scroll = ${info.metrics.pixels}');
bool isFloat = info.metrics.pixels>57.0;
if(_isFloat!=isFloat){
_isFloat= isFloat;
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
setState((){});
});
}
return true;
},
child: NestedScrollView(
floatHeaderSlivers: false,
headerSliverBuilder: (context, isInnerScroll) {
return [
SliverAppBar(
title: Text('Demo'),
floating: _isFloat,
pinned: !_isFloat,
),
];
},
body: SmartRefresher(
controller: _refreshController,
onRefresh: () {
Future.delayed(Duration(milliseconds: 1000), () {
_refreshController.refreshCompleted();
});
},
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return ListTile(
title: Text('$index'),
);
},
),
),
),
),
);
}
),
);

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.

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