I am trying to create an app with a top application bar and a tab bar below. When you scroll down, the bar should hide by moving off the screen (but tabs should stay), and w
I was able to make the floating Appbar with Tabbar similar to that of WhatsApp by using SliverAppbar with NestedScrollView.
Do add floatHeaderSlivers: true, in NestedScrollView and
pinned: true, floating: true, in SliverAppBar
Link to working code sample
import 'dart:math';
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(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CustomSliverAppbar(),
);
}
}
class CustomSliverAppbar extends StatefulWidget {
@override
_CustomSliverAppbarState createState() => _CustomSliverAppbarState();
}
class _CustomSliverAppbarState extends State<CustomSliverAppbar>
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
_tabController = TabController(
initialIndex: 0,
length: 2,
vsync: this,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text(
"WhatsApp type sliver appbar",
),
centerTitle: true,
pinned: true,
floating: true,
bottom: TabBar(
indicatorColor: Colors.black,
labelPadding: const EdgeInsets.only(
bottom: 16,
),
controller: _tabController,
tabs: [
Text("TAB A"),
Text("TAB B"),
]),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
TabA(),
const Center(
child: Text('Display Tab 2',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
],
),
),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
class TabA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.separated(
separatorBuilder: (context, child) => Divider(
height: 1,
),
padding: EdgeInsets.all(0.0),
itemCount: 30,
itemBuilder: (context, i) {
return Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
);
},
),
);
}
}
--- EDIT 1 --
Alright so I threw together something quick for you. I followed this article (written by Emily Fortuna who is one of the main Flutter devs) to better understand Slivers.
Medium: Slivers, Demystified
But then found this Youtube video that basically used your code so I opted for this one, rather than try to figure out every small detail about Slivers.
Youtube: Using Tab and Scroll Controllers and the NestedScrollView in Dart's Flutter Framework
Turns out you were on the right track with your code. You can use SliverAppBar
within NestedScrollView
(this wasn't the case last time I tried) but I made a few changes. That I will explain after my code:
import 'package:flutter/material.dart';
import 'dart:math';
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,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
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 /*<-- This is for the controllers*/ {
TabController _tabController; // To control switching tabs
ScrollController _scrollViewController; // To control scrolling
List<String> items = [];
List<Color> colors = [Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.blue, Colors.amber, Colors.cyan, Colors.pink];
Random random = new Random();
Color getRandomColor() {
return colors.elementAt(random.nextInt(colors.length));
}
@override
void initState() {
super.initState();
_tabController =TabController(vsync: this, length: 2);
_scrollViewController =ScrollController();
}
@override
void dispose() {
super.dispose();
_tabController.dispose();
_scrollViewController.dispose();
}
@override
Widget build(BuildContext context) {
// Init the items
for (var i = 0; i < 100; i++) {
items.add('Item $i');
}
return SafeArea(
child: NestedScrollView(
controller: _scrollViewController,
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
title: Text("WhatsApp using Flutter"),
floating: true,
pinned: false,
snap: true,
bottom: TabBar(
tabs: <Widget>[
Tab(
child: Text("Colors"),
),
Tab(
child: Text("Chats"),
),
],
controller: _tabController,
),
),
];
},
body: TabBarView(
controller: _tabController,
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
Color color = getRandomColor();
return Container(
height: 150.0,
color: color,
child: Text(
"Row $index",
style: TextStyle(
color: Colors.white,
),
),
);
},
//physics: NeverScrollableScrollPhysics(), //This may come in handy if you have issues with scrolling in the future
),
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Material(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blueGrey,
),
title: Text(
items.elementAt(index)
),
),
);
},
//physics: NeverScrollableScrollPhysics(),
),
],
),
),
);
}
}
Alright so on to the explanation.
Use a StatefulWidget
Most widgets in Flutter are going to be stateful but it depends on the situation. I think in this case it is better because you are using a ListView
which could change as users add or erase conversations/chats.
SafeArea
because this widget is great.
Go read about it on Flutter Docs: SafeArea
The Controllers
I think this was the big issue at first but maybe it was something else. But you should usually make your own controllers if you are dealing with custom behavior in Flutter. So I made the _tabController
and the _scrollViewController
(I don't think I got every bit of functionality out of them, i.e. keeping track of scroll positions between tabs, but they work for the basics). The tab controller that you use for the TabBar
and the TabView
should be the same.
The Material
Widget before the ListTile
You probably would have found this out sooner or later but the ListTile
widget is a Material widget and therefore requires a "Material ancestor widget" according to the output I got while trying to render it at first. So I saved you a tiny headache with that. I think it is because I didn't use a Scaffold
. (Just keep this in mind when you use Material widgets without Material ancestor widgets)
Hope this helps you get started, if you need any assistance with it just message me or add me to your Github repo and I'll see what I can do.
--- ORIGINAL ---
I answered you on Reddit as well, hopefully you see one of these two soon.
SliverAppBar Info
The key properties you want to have with the SliverAppBar are:
floating: Whether the app bar should become visible as soon as the user scrolls towards the app bar.
pinned: Whether the app bar should remain visible at the start of the scroll view. (This is the one you are asking about)
snap: If snap and floating are true then the floating app bar will "snap" into view.
All this came from the Flutter SliverAppBar Docs. They have lots of animated examples with different combos of floating, pinned and snap.
So for yours the following should work:
SliverAppBar(
title: Text("Application"),
floating: true, // <--- this is required if you want the appbar to come back into view when you scroll up
pinned: false, // <--- this will make the appbar disappear on scrolling down
snap: true, // <--- this is required if you want the application bar to 'snap' when you scroll up (floating MUST be true as well)
bottom: new TabBar(
tabs: [ ... ], // <-- total of 2 tabs
),
),
ScrollView with SliverAppBar
To answer the underlying question of the NestedScrollView
. According to the docs (same as above) a SliverAppBar
is:
A material design app bar that integrates with a
CustomScrollView
.
Therefore you cannot use a This is the intended use of NestedScrollView
you need to use a CustomScrollView
.Sliver
classes but they can be used in the NestedScrollView
Check out the docs.
You need to use SliverOverlapAbsorber/SliverOverlapInjector, the following code works for me (Full Code):
@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),
child: 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(),
),
),
),
),
);
}