I've created two tabs.
In each tab I have SingleChildScrollView wrapped with Scrollbar.
I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn't respond to keyboard page up /down keys.
Please check my code below. Guide me on how to achieve keyboard scrolling for Tab TWO.
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(
home: TabExample(),
);
}
}
class TabExample extends StatefulWidget {
const TabExample({Key key}) : super(key: key);
#override
_TabExampleState createState() => _TabExampleState();
}
class _TabExampleState extends State<TabExample> {
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController();
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
_buildWidgetA(),
_buildWidgetB(),
],
),
),
);
}
Widget _buildWidgetA() {
List<Widget> children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.black,
),
),
);
}
return Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
Widget _buildWidgetB() {
List<Widget> children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.green,
),
),
);
}
return Scrollbar(
controller: _scrollController,
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: children,
),
),
);
}
}
You don't need to create an explicit ScrollController to achieve this.
One trick is to change which SingleChildScrollView is going to use the PrimaryScrollController whenever the Tab changes it's index.
So, when we listen that tab has changed to index 0, we will set that the first SingleChildScrolView is the primary one. When it changes to 1, we will set the other on as primary.
First create a new State variable like this,
int currentIndex = 0; // This will be the index of tab at a point in time
To listen to the change event, you need to add Listener to the TabController.
DefaultTabController(
length: 2,
child: Builder( // <---- Use a Builder Widget to get the context this this DefaultTabController
builder: (ctx) {
// Here we need to use ctx instead of context otherwise it will give null
final TabController tabController = DefaultTabController.of(ctx);
tabController.addListener(() {
if (!tabController.indexIsChanging) {
// When the tab has changed we are changing our currentIndex to the new index
setState(() => currentIndex = tabController.index);
}
});
return Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
_buildWidgetA(),
_buildWidgetB(),
],
),
);
},
),
);
Finally, depending on the currentIndex set primary: true to each SingleChildScrollView.
For _buildWidgetA,
Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
primary: currentIndex == 0, // <--- This will be primary if currentIndex = 0
child: Column(
children: children,
),
),
);
For _buildWidgetB,
Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
primary: currentIndex == 1, // <--- This will be primary if currentIndex = 1
child: Column(
children: children,
),
),
);
Now, you should be able to control both of the tabs with your keyboard.
Full code here
Related
I have a requirement to build a view that has a tab bar within a collapsible app bar (so when the app bar collapses, just the tabs are visible) which controls a tab view, each of which will host a different sub view. Further, within those subviews on each tab, the content should be scrollable (which impacts the collapsed state of the app bar), and widgets within that scrollable view need to have headers that "stick" to the bottom of the app bar/tab bar.
A sliver-based implementation of sticky headers are available here: Flutter Sticky Headers
I took the first sample from Flutter's documentation on this page:
https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html#widgets.NestedScrollView.1
and wrapped the SliverFixedExtentList in a SliverStickyHeader element and the headers are not sticking to the top of the list. I've tried this in a variety of places but can't seem to make a sticky header work using this library or even the original which used RenderObjects. Any thoughts as to why this might be?
Here's the full code as an example:
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
void main() => runApp(const MyApp());
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(),
);
}
}
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,
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: SliverStickyHeader(
header: Text('header'),
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: 30,
),
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}
I think the problem has something to do with the SliverOverlapInjector there is a workaround mentioned in this issue comment: https://github.com/flutter/flutter/issues/91972#issuecomment-995462622
Basically you need an improved overlap injector ( SliverPinnedOverlapInjector):
DISCLAIMER: The following source code is directly copied from the issue comment:
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class SliverPinnedOverlapInjector extends SingleChildRenderObjectWidget {
const SliverPinnedOverlapInjector({
required this.handle,
Key? key,
}) : super(key: key);
final SliverOverlapAbsorberHandle handle;
#override
RenderSliverPinnedOverlapInjector createRenderObject(BuildContext context) {
return RenderSliverPinnedOverlapInjector(
handle: handle,
);
}
#override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedOverlapInjector renderObject,
) {
renderObject.handle = handle;
}
}
class RenderSliverPinnedOverlapInjector extends RenderSliver {
RenderSliverPinnedOverlapInjector({
required SliverOverlapAbsorberHandle handle,
}) : _handle = handle;
double? _currentLayoutExtent;
double? _currentMaxExtent;
SliverOverlapAbsorberHandle get handle => _handle;
SliverOverlapAbsorberHandle _handle;
set handle(SliverOverlapAbsorberHandle value) {
if (handle == value) return;
if (attached) {
handle.removeListener(markNeedsLayout);
}
_handle = value;
if (attached) {
handle.addListener(markNeedsLayout);
if (handle.layoutExtent != _currentLayoutExtent ||
handle.scrollExtent != _currentMaxExtent) markNeedsLayout();
}
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
handle.addListener(markNeedsLayout);
if (handle.layoutExtent != _currentLayoutExtent ||
handle.scrollExtent != _currentMaxExtent) markNeedsLayout();
}
#override
void detach() {
handle.removeListener(markNeedsLayout);
super.detach();
}
#override
void performLayout() {
_currentLayoutExtent = handle.layoutExtent;
final paintedExtent = min(
_currentLayoutExtent!,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: paintedExtent,
maxPaintExtent: _currentLayoutExtent!,
maxScrollObstructionExtent: _currentLayoutExtent!,
paintOrigin: constraints.overlap,
scrollExtent: _currentLayoutExtent!,
layoutExtent: max(0, paintedExtent - constraints.scrollOffset),
hasVisualOverflow: paintedExtent < _currentLayoutExtent!,
);
}
}
Just copy the SliverPinnedOverlapInjector to your project and use it as a drop-in replacement for the SliverOverlapInjector. In my case this was the only change necessary. Everything else should stay the same.
Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
centerTitle: false,
automaticallyImplyLeading: false,
backgroundColor: Colors.black,
title: Text('Murder series (Event That Happen)'),
actions: <Widget>[
],
child: Container(
height: 20,
width: 20,
child: SvgPicture.asset(
'assets/library_pre.svg',
color: Colors.white,
),
),
),
),
],
expandedHeight: 200.0,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Column(
children: [
SizedBox(
height: 70,
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CircleAvatar(
radius: 40,
backgroundImage:
AssetImage('assets/forumPage.png'),
),
Text(
'74 Members',
style: TextStyle(
fontSize: 25,
color: Colors.white,
),
),
overlapped(),
Text(
'Leave',
style: TextStyle(
fontSize: 25,
color: Colors.red,
),
)
],
),
),
),
Container(
height: 30,
width: double.infinity,
decoration: BoxDecoration(color: Colors.red),
)
],
),
)),
),
];
},
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
],
),
],
),
))),
);
I am using hooks for handling tab controller, the problem is when i print the current index of the controller it is right. But when i try to display it inside a text widget it is never changes! how could i use it?
The code is:
class TabBarDemo extends HookWidget {
final List<Widget> list = const [
Tab(icon: Icon(Icons.card_travel)),
Tab(icon: Icon(Icons.add_shopping_cart)),
Tab(icon: Icon(Icons.ac_unit)),
];
#override
Widget build(BuildContext context) {
final _controller =
useTabController(initialLength: list.length, initialIndex: 0);
_controller.addListener(() {
print("\n ${_controller.index} \n");
});
return Scaffold(
appBar: AppBar(
bottom: TabBar(
onTap: (index) {},
controller: _controller,
tabs: list,
),
),
body: TabBarView(
controller: _controller,
children: [
Center(
child: Text(
'${_controller.index}',
)),
Center(
child: Text(
'${_controller.index}',
)),
Center(
child: Text(
'${_controller.index}',
)),
],
),
);
}
}
You have to rebuild the tree using :
setState(() {
});
Iam trying to implement a appbar like this
When scrolling down I need to hide the search bar alone and pin the row and the tabs on the device top. Which is like
And when we scroll down the all the three rows needs to be displayed.
Using SliverAppBar with bottom property tabs are placed and pinned when scrolling, but a row above it should be pinned at the top above the tabbar. Im not able to add a column with the row and tabbar because of preferedSizeWidget in bottom property. Flexible space bar also hides with the appbar so I cannot use it. Does anyone know how to make this layout in flutter.
Please try this.
body: Container(
child: Column(
children: <Widget>[
Container(
// Here will be your AppBar/Any Widget.
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
// All your scroll views
Container(),
Container(),
],
),
),
),
],
),
),
You could create your own SliverAppBar or you can divide them in 2 items, a SliverAppBar and a SliverPersistentHeader
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home>
with SingleTickerProviderStateMixin {
TabController controller;
TextEditingController textController = TextEditingController();
#override
void initState() {
super.initState();
controller = TabController(
length: 3,
vsync: this,
);
}
#override
void dispose(){
super.dispose();
controller.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(color: Colors.black.withOpacity(.5), fontSize: 16),
border: InputBorder.none
)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
//This is Where you create the row and your tabBar
SliverPersistentHeader(
delegate: MyHeader(
top: Row(
children: [
for(int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
)
)
]
),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
controller: controller,
),
),
pinned: true,
),
SliverFillRemaining(
child: TabBarView(
controller: controller,
children: <Widget>[
Center(child: Text("Tab one")),
Center(child: Text("Tab two")),
Center(child: Text("Tab three")),
],
),
),
],
),
);
}
}
//Your class should extend SliverPersistentHeaderDelegate to use
class MyHeader extends SliverPersistentHeaderDelegate {
final TabBar bottom;
final Widget top;
MyHeader({this.bottom, this.top});
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).accentColor,
height: math.max(minExtent, maxExtent - shrinkOffset),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if(top != null)
SizedBox(
height: kToolbarHeight,
child: top
),
if(bottom != null)
bottom
]
)
);
}
/*
kToolbarHeight = 56.0, you override the max and min extent with the height of a
normal toolBar plus the height of the tabBar.preferredSize
so you can fit your row and your tabBar, you give them the same value so it
shouldn't shrink when scrolling
*/
#override
double get maxExtent => kToolbarHeight + bottom.preferredSize.height;
#override
double get minExtent => kToolbarHeight + bottom.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => false;
}
UPDATE
A NestedScollView let you have 2 ScrollViews so you can control the inner scroll with the outer (just like you want with a TabBar)
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController textController = TextEditingController();
List<String> _tabs = ['Tab 1', 'Tab 2', 'Tab 3'];
// Your tabs, or you can ignore this and build your list
// on TabBar and the TabView like my previous example.
// I don't create a TabController now because I wrap the whole widget with a DefaultTabController
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
textController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length, // This is the number of tabs.
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
elevation: 0.0,
leading: const Icon(Icons.menu),
title: TextField(
controller: textController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
hintText: 'Search Bar',
hintStyle: TextStyle(
color: Colors.black.withOpacity(.5),
fontSize: 16),
border: InputBorder.none)
),
snap: true,
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print('searching for: ${textController.text}'),
)
]
),
),
SliverPersistentHeader(
delegate: MyHeader(
top: Row(children: [
for (int i = 0; i < 4; i++)
Expanded(
child: OutlineButton(
child: Text('button $i'),
onPressed: () => print('button $i pressed'),
))
]),
bottom: TabBar(
indicatorColor: Colors.white,
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
pinned: true,
),
];
},
body: TabBarView(
children: _tabs.map((String name) {
return SafeArea(
child: Builder(
// This Builder is needed to provide a BuildContext that is
// "inside" the NestedScrollView, so that
// sliverOverlapAbsorberHandleFor() can find the
// NestedScrollView.
// You can ignore it if you're going to build your
// widgets in another Stateless/Stateful class.
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),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
onTap: () => print('$name at index $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
));
}
}
import 'dart:io';
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(
primaryColor: Colors.white,
),
home: NewsScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class NewsScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _NewsScreenState();
}
class _NewsScreenState extends State<NewsScreen> {
final List<String> _tabs = <String>[
"Featured",
"Popular",
"Latest",
];
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverSafeArea(
top: false,
bottom: Platform.isIOS ? false : true,
sliver: SliverAppBar(
title: Text('Tab Demo'),
elevation: 0.0,
floating: true,
pinned: true,
snap: true,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
),
];
},
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
),
);
}
}
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,
),
);
}
}
I'm trying to create a screen that is contained within a pageview, that also contains a page view for part of the screen.
To acheive this I have an unlimited page view for the whole page itself, then every page has a header view, with a bottom half that has a page view with 3 possible options. I have this pretty much working, however, the pages I am using I would like a StreamBuilder... This is where the issue is caused.
class DiaryPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _DiaryPage();
}
class _DiaryPage extends State<DiaryPage> with TickerProviderStateMixin {
DiaryBloc _diaryBloc;
TabController _tabController;
PageController _pageController;
#override
void initState() {
_diaryBloc = BlocProvider.of<DiaryBloc>(context);
_diaryBloc.init();
_tabController = TabController(length: 3, vsync: this);
_pageController = PageController(initialPage: _diaryBloc.initialPage);
super.initState();
}
#override
void dispose() {
_diaryBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Flexible(
child: PageView.builder(
controller: _pageController,
itemBuilder: (BuildContext context, int position) {
return _buildPage(_diaryBloc.getDateFromPosition(position));
},
itemCount: _diaryBloc.amountOfPages,
),
);
}
Widget _buildPage(DateTime date) {
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[_getHeader(date), _getTabBody()],
);
}
Widget _getHeader(DateTime date) {
return Card(
child: SizedBox(
width: double.infinity,
height: 125,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Text(
'${DateFormat('EEEE').format(date)} ${date.day} ${DateFormat('MMMM').format(date)}',
style: Theme.of(context).textTheme.subtitle,
textScaleFactor: 1,
textAlign: TextAlign.center,
),
),
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => {
_pageController.previousPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
const Expanded(child: LinearProgressIndicator()),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => {
_pageController.nextPage(
duration: Duration(milliseconds: 250),
curve: Curves.ease)
},
),
],
),
Container(
height: 40.0,
child: DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: Colors.white,
appBar: TabBar(
controller: _tabController,
unselectedLabelColor: Colors.grey[500],
labelColor: Theme.of(context).primaryColor,
tabs: const <Widget>[
Tab(icon: Icon(Icons.pie_chart)),
Tab(icon: Icon(Icons.fastfood)),
Tab(icon: Icon(Icons.directions_run)),
],
),
),
),
),
],
),
),
);
}
Widget _getTabBody() {
return Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
_getOverviewScreen(),
_getFoodScreen(),
_getExerciseScreen(),
],
),
);
}
// TODO - this seems to be the issue, wtf and why
Widget _getBody() {
return Flexible(
child: StreamBuilder<Widget>(
stream: _diaryBloc.widgetStream,
initialData: _diaryBloc.buildEmptyWidget(),
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
return snapshot.data;
},
),
);
}
Widget _getExerciseScreen() {
return Text("Exercise Screen"); //_getBody();
}
Widget _getFoodScreen() {
return Text("Food Screen"); //_getBody();
}
Widget _getOverviewScreen() {
return _getBody();
}
}
As you can see, there are three widgets being returned as part of the sub page view, 2 of them are Text Widgets which show correctly, but the StreamBuilder, which is populated correctly with another Text Widget seems to give me the red screen of death. Any ideas?
Fixed the problem, it was related to the StreamBuilder being wrapped in a Flexible rather than a column. I then added column to have a mainAxisSize of max... Seemed to work.
For custom ListView/PageView
In my case, I wanted to clear the list of my listview. In a custom ListView/PageView, the findChildIndexCallback will find the element's index after i.e. a reordering operation, but also when you clear the list.
yourList.indexWhere()unfortunately returns -1 when it couldn't find an element. So, Make sure to return null in that case, to tell the callback that the child doesn't exist anymore.
...
findChildIndexCallback: (Key key) {
final ValueKey<String> valueKey = key as ValueKey<String>;
final data = valueKey.value;
final index = images.indexWhere((element) => element.id == data);
//important here:
if (index > 0 ) return index;
else return null;
},