TabBarView with variable height inside a ListView

12,018

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.

enter image description here

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

Share:
12,018

Related videos on Youtube

danle
Author by

danle

Updated on September 14, 2022

Comments

  • danle
    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
    Like this
    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
      Rémi Rousselet over 5 years
      I 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
    NIRAV PATEL about 5 years
    @pierre Used ListView widget & inside that add DummySection widget which one have Container with some height.( You can also add container directly in listview.) For TabarView i added Container widget with some heigh. Inside that used column for Tab & TabBarView.
  • danle
    danle about 5 years
    My 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
    Vladimir Koltunov about 4 years
    Do dot forget tabController.dispose();
  • Alann Maulana
    Alann Maulana about 4 years
    Yes, override it at dispose method. Thanks
  • ricardogobbo
    ricardogobbo over 3 years
    That's an awesome solution. Thanks!
  • Arpit
    Arpit over 3 years
    This is awesome. Though is it possible to make them scrollable?
  • Alann Maulana
    Alann Maulana over 3 years
    Of course, you can change the child container using listview or singlechildscrollview with column
  • Pedro Varela
    Pedro Varela about 3 years
    Perfect 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
    Alann Maulana about 3 years
    Yes, 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
    Dani almost 3 years
    you lose the horizontal scroll with this
  • Alann Maulana
    Alann Maulana almost 3 years
    Yea, you're right. I use a lot this workaround when having multiple widget of list, grid and tab on a single page.
  • Dani
    Dani almost 3 years
    you know why I get "The argument type '_RentScreenState' can't be assigned to the parameter type 'TickerProvider'" with vsync: this?
  • Bishal
    Bishal about 2 years
    @Dani you have to add with SingleTickerProviderStateMixin to the end of your State’s class declaration.
  • Dani
    Dani about 2 years
    tbh I don't remember. Only thing I know now is that it works well :D