TabBarView with variable height inside a ListView
Solution 1
You don't need to have TabView to show Tabs content. the minus of this approcach that you are loosing animations and swipes, so you will need to do it by your self if you realy will need it.
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
final List<Widget> myTabs = [
Tab(text: 'one'),
Tab(text: 'two'),
Tab(text: 'three'),
];
TabController _tabController;
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(_handleTabSelection);
super.initState();
}
_handleTabSelection() {
if (_tabController.indexIsChanging) {
setState(() {});
}
}
_listItem() {
return Container(
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Colors.blueAccent,
),
),
height: 120,
child: Center(
child: Text('List Item', style: TextStyle(fontSize: 20.0)),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: <Widget>[
_listItem(),
TabBar(
controller: _tabController,
labelColor: Colors.redAccent,
tabs: myTabs,
),
Center(
child: [
Text('first tab'),
Column(
children: [
Text('second tab'),
...List.generate(10, (index) => Text('line: $index'))
],
),
Column(
children: [
Text('third tab'),
...List.generate(20, (index) => Text('line: $index'))
],
),
][_tabController.index],
),
_listItem(),
_listItem(),
],
),
);
}
}
Solution 2
Don't use TabBarView
, use IndexedStack
with Visibility
instead.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
TabController tabController;
int selectedIndex = 0;
@override
void initState() {
super.initState();
tabController = TabController(
initialIndex: selectedIndex,
length: 2,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: ListView(
children: [
Container(
height: 128,
color: Colors.blue,
),
Container(
height: 256,
color: Colors.green,
),
TabBar(
tabs: <Tab>[
Tab(text: 'Tab Left'),
Tab(text: 'Tab Right'),
],
controller: tabController,
onTap: (int index) {
setState(() {
selectedIndex = index;
tabController.animateTo(index);
});
},
),
Divider(height: 0),
IndexedStack(
children: <Widget>[
Visibility(
child: Container(
height: 200,
color: Colors.yellow,
child: Center(
child: Text('Content left'),
),
),
maintainState: true,
visible: selectedIndex == 0,
),
Visibility(
child: Container(
height: 1000,
color: Colors.red,
child: Center(
child: Text('Content right'),
),
),
maintainState: true,
visible: selectedIndex == 1,
),
],
index: selectedIndex,
),
],
),
),
);
}
}
IndexedStack
will show a single child from a list of children based on index
while Visibility
will maintain to show or hide view. When the view is hiding, so there is no excess white space to show (stack height is equal to the maximum height of its children).
Here is the dartpad https://dartpad.dev/535f06aa01257b049c7f2f9c719c9881.
Solution 3
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return HomeState();
}
}
class HomeState extends State<Home> with SingleTickerProviderStateMixin {
TabController tabController;
@override
void initState() {
// TODO: implement initState
super.initState();
tabController = TabController(length: 3, vsync: this, initialIndex: 0);
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: ListView(
children: <Widget>[
DummySection(height: 100.0,color: Colors.red,),
DummySection(height: 150.0,color: Colors.yellow,),
Container(
height: 350.0,
color: Colors.blue,
child: Column(
children: <Widget>[
TabBar(
unselectedLabelColor: Colors.blue[100],
indicator: BoxDecoration(
color: Colors.lightBlue
),
controller: tabController,
tabs: <Widget>[
Tab(text: "Home",),
Tab(text: "Fav",),
Tab(text: "Star",)
],
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
DummyList(),
DummyList(),
DummyList()
]
),
)
],
),
),
DummySection(height: 100.0,color: Colors.red,),
DummySection(height: 100.0,color: Colors.pink,)
],
),
);
}
}
// Dummy List Container
class DummySection extends StatelessWidget{
Color color;
double height;
DummySection({this.color,this.height});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
color: color,
height: height,
);
}
}
// Dummy Listing for tab
class DummyList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView(
children: <Widget>[
Card(
child: Container(
height: 200.0,
alignment: Alignment.center,
child: Text("hello"),
),
),
Card(
child: Container(
height: 200.0,
alignment: Alignment.center,
child: Text("hello"),
),
),
],
);
}
}
Related videos on Youtube
danle
Updated on September 14, 2022Comments
-
danle over 1 year
I have a ListView with multiple type of items, one of which is a widget of TabBar and TabBarView.
The problem is each tab page's height is different and I want the ListView to wrap the tab widget dynamically according to it's height
But the TabBarView doesn't accept unbounded height and ListView can't provide a height for it's children.
Is there anyway this can be done? Or must I use the TabBar with something that can wrap it's content like a Column and sacrifice the ability to swipe between tabs?-
Rémi Rousselet over 5 yearsI do not think this is possible with TabBarView. It is made to be used asa view, not as an inline content. Making your own should be easy though
-
-
NIRAV PATEL about 5 years@pierre Used
ListView
widget & inside that addDummySection
widget which one haveContainer
with some height.( You can also add container directly in listview.) For TabarView i addedContainer
widget with some heigh. Inside that used column forTab & TabBarView
. -
danle about 5 yearsMy intention is for the TabBarView to change it's height based on it's current child content. With your code, the TabBarView will have a fixed-height of (350 - TabBar height) and will be overflowed if it's child's height is larger than that.
-
Vladimir Koltunov about 4 yearsDo dot forget
tabController.dispose();
-
Alann Maulana about 4 yearsYes, override it at dispose method. Thanks
-
ricardogobbo over 3 yearsThat's an awesome solution. Thanks!
-
Arpit over 3 yearsThis is awesome. Though is it possible to make them scrollable?
-
Alann Maulana over 3 yearsOf course, you can change the child container using listview or singlechildscrollview with column
-
Pedro Varela about 3 yearsPerfect solution, but this can be improved. There is no need to wrap the children with Visibility, the IndexStack will only display one child at a time given by its index property.
-
Alann Maulana about 3 yearsYes, but all of children will have the same height. Visibility will do the trick to hide not visible child. But you can remove it if you don't mind having same height to all children.
-
Dani almost 3 yearsyou lose the horizontal scroll with this
-
Alann Maulana almost 3 yearsYea, you're right. I use a lot this workaround when having multiple widget of list, grid and tab on a single page.
-
Dani almost 3 yearsyou know why I get "The argument type '_RentScreenState' can't be assigned to the parameter type 'TickerProvider'" with vsync: this?
-
Bishal about 2 years@Dani you have to
add with SingleTickerProviderStateMixin
to the end of your State’s class declaration. -
Dani about 2 yearstbh I don't remember. Only thing I know now is that it works well :D