タブ切り替えをStateful Widgetで実装する方法を紹介します。
Stateful Widgetの場合はDefaultTabControllerを使わなくてもタブ切り替えを実装することが可能です。DefaultTabControllerを使ったケースはこちらをご覧ください。
まずはサンプルコードの全体をご覧ください。
import 'package:flutter/material.dart';void main() {  runApp(const MyApp());}class MyApp extends StatelessWidget {  const MyApp({Key? key}) : super(key: key);  @override  Widget build(BuildContext context) {    return MaterialApp(      debugShowCheckedModeBanner: false,      title: 'タブバーを試す',      theme: ThemeData(        primarySwatch: Colors.blue,      ),      home: const TabBarScreen(),    );  }}class TabBarScreen extends StatefulWidget {  const TabBarScreen({Key? key}) : super(key: key);  @override  _TabBarScreenState createState() => _TabBarScreenState();}class _TabBarScreenState extends State<TabBarScreen>    with TickerProviderStateMixin {  late TabController _tabController;  @override  void initState() {    super.initState();    _tabController = TabController(length: 3, vsync: this);  }  @override  void dispose() {    _tabController.dispose();    super.dispose();  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: const Text('タブバーのサンプル'),        bottom: TabBar(          controller: _tabController,          tabs: const [            Tab(              icon: Icon(Icons.home),              text: 'ホーム',            ),            Tab(              icon: Icon(Icons.access_time_sharp),              text: '通知',            ),            Tab(              icon: Icon(Icons.build),              text: '設定',            ),          ],          onTap: (index) {            print('$index番目の画面になりました');          },        ),      ),      body: TabBarView(        controller: _tabController,        children: const [          Center(            child: Text(              'ホーム画面',              style: TextStyle(fontSize: 40.0),            ),          ),          Center(            child: Text(              '通知画面',              style: TextStyle(fontSize: 40.0),            ),          ),          Center(            child: Text(              '設定画面',              style: TextStyle(fontSize: 40.0),            ),          ),        ],      ),    );  }}ひとつひとつポイントを見ていきましょう。
以下の30行目からはじまる_TabBarScreenStateのコードを見てください。
class _TabBarScreenState extends State<TabBarScreen>    with TickerProviderStateMixin {  late TabController _tabController;  @override  void initState() {    super.initState();    _tabController = TabController(length: 3, vsync: this);  }32行目のところでTabConrtollerの変数を保持しています。
この_tabControllerがタブの切り替えを制御するクラスです。
Stateless Widgetのケースで使用していたDefaultControllerの代わりに必要になります。
37行目のようにinitStateの中でTabControllerの実体を作成しています。
このとき、length:で必要なタブの数を指定します。
この37行目のコードを書くのに必要なのが、31行目で指定しているTickerProviderStateMixinです。
タブの切り替え時にアニメーションの演出が入りますが、そのために必要なパラメーターです。TabControllerは内部でAnimationControllerを使用していて、vsync:にアニメーションを動作させるクラスのインスタンスを指定してあげます。この場合はthis = _TabBarScreenStateですね。
あとは、タブの具体的な準備です。
以下のコードを見てください。
      appBar: AppBar(        title: const Text('タブバーのサンプル'),        bottom: TabBar(          controller: _tabController,          tabs: const [            Tab(              icon: Icon(Icons.home),              text: 'ホーム',            ),            Tab(              icon: Icon(Icons.access_time_sharp),              text: '通知',            ),            Tab(              icon: Icon(Icons.build),              text: '設定',            ),          ],          onTap: (index) {            print('$index番目の画面になりました');          },        ),      ),AppBarのbottom:の部分にタブバーの具体的な設定を書いていきます。
52行目で_tabControllerを指定することで、DefaultTabControllerを使わずにタブ切り替えを実現できます。
また、67行目のようにonTap:でタブを切り替えたときのイベントを受け取ることが可能です。indexには切り替え後のタブ番号が設定されてきます。
タブごとの内容表示も必要です。
以下のコードを見てください。
      body: TabBarView(        controller: _tabController,        children: const [          Center(            child: Text(              'ホーム画面',              style: TextStyle(fontSize: 40.0),            ),          ),          Center(            child: Text(              '通知画面',              style: TextStyle(fontSize: 40.0),            ),          ),          Center(            child: Text(              '設定画面',              style: TextStyle(fontSize: 40.0),            ),          ),        ],      ),ここでも73行目で_tabControllerを指定しています。children:にはタブごとの表示内容を設定します。
今回はサンプルなので、CenterとTextのシンプルな内容ですが、
実際にはかなり複雑化すると思います。
タブごとに別のWidgetとして切り出して読み込む形にするのが現実的です。DefaultTabControllerを使うパターンよりもコードのネストが1階層減るため見やすくなるのも利点だと思います。
Flutterのコードはただでさえネストが激しくなりがちなので・・