Home > front end >  Flutter: DefaulltTabController with single child TabBarView
Flutter: DefaulltTabController with single child TabBarView

Time:01-08

I use the following code snippet to create a tab bar with 20 tabs along with their views (you can copy-paste the code to try it out, it complies with no problems):

import 'package:flutter/material.dart';

class TabBody extends StatefulWidget {
  final int tabNumber;
  const TabBody({required this.tabNumber, Key? key}) : super(key: key);

  @override
  State<TabBody> createState() => _TabBodyState();
}

class _TabBodyState extends State<TabBody> {
  @override
  void initState() {
    print(
        'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
    super.initState();
  }

  getDataForTab() {
    //getting data for widget.tabNumber
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          color: Colors.grey,
          child: Text('This is tab #${widget.tabNumber} body')),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  List<Text> get _tabs {
    var list = [for (var i = 0; i < 20; i  = 1) i];
    List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
    return tabs;
  }

  List<TabBody> get _tabsBodies {
    var list = [for (var i = 0; i < 20; i  = 1) i];
    List<TabBody> bodies = list.map((i) => TabBody(tabNumber: i)).toList();
    return bodies;
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Column(
        children: <Widget>[
          Container(
            width: double.infinity,
            height: 50,
            color: Colors.black,
            child: TabBar(
              isScrollable: true,
              tabs: _tabs,
            ),
          ),
          Expanded(
            child: TabBarView(
              children: _tabsBodies, //<--- i want this to be one child only
            ),
          )
        ],
      ),
    );
  }
}

I need to do the following but couldn't find a way for that:

  1. I want to let the TabBarView to have only one child of type TabBody not a list of _tabsBodies, i.e. the print statement in initState should execute once.

  2. I want to execute the function getDataForTab every time the tab is changed to another tab.

so in general I need to refresh the tab body page for each tab selection, in contrast to the default implementation of the DefaultTabController widget which requires to have n number of tab bodies for n number of tabs.

CodePudding user response:

You'll need to do three things:

  • Remove the TabBarView. You don't need it if you want to have a single widget. (Having a TabBar does not require you to have a TabBarView)

  • Create your own TabController so you can pass the current index to the TabBody.

  • Listen to the TabController and update the state to pass the new index to TabBody.

Here's a fully runnable example and that you can copy and paste to DartPad

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage>
    with SingleTickerProviderStateMixin {
  late final tabController =
      TabController(length: 20, vsync: this, initialIndex: 0);

  @override
  void initState() {
    super.initState();
    tabController.addListener(() {
      if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
        print('setting state'); // <~~ will print one time now
        setState(() {});
      }
    });
  }

  List<Text> get _tabs {
    var list = [for (var i = 0; i < 20; i  = 1) i];
    List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
    return tabs;
  }

  @override
  void dispose() {
    tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          width: double.infinity,
          height: 50,
          color: Colors.black,
          child: TabBar(
              isScrollable: true, tabs: _tabs, controller: tabController),
        ),
        Expanded(
          child: TabBody(tabNumber: tabController.index),
        )
      ],
    );
  }
}

class TabBody extends StatefulWidget {
  final int tabNumber;
  const TabBody({required this.tabNumber, Key? key}) : super(key: key);

  @override
  State<TabBody> createState() => _TabBodyState();
}

class _TabBodyState extends State<TabBody> {
  @override
  void initState() {
    print(
        'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
    super.initState();
  }

  getDataForTab() {
    //getting data for widget.tabNumber
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          color: Colors.grey,
          child: Text('This is tab #${widget.tabNumber} body')),
    );
  }
}

Few notes about the example:

  • when you create a TabController, you'll need a ticker. You can use SingleTickerProviderStateMixin to make the class itself a ticker (hence: vsync: this). Alternatively, you can create your own and pass it to TabController.vsync parameter.
class _MainPageState extends State<MainPage> with SingleTickerProviderStateMixin {
  late final tabController = TabController(length: 20, vsync: this, initialIndex: 0);
  • Here we are listening to the tab controller whenever the tabs changes:
  @override
  void initState() {
    super.initState();
    tabController.addListener(() {
      if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
        print('setting state'); // <~~ will print one time now
        setState(() {});
      }
    });
  }

edit: you'll also need to dispose the tabController. I updated the code above.

  •  Tags:  
  • Related