[Flutter] 上タブ(TabBar)を使いたい

[Flutter] 上タブ(TabBar)を使いたい

Clock Icon2025.06.28

こんにちは。きんくまです。
今回は上タブ(TabBar)を使ってみようと思い調べてみました。

作ったもの

iOS Android
テキスト tabbar1_250628 tabbar2_250628
アイコン画像 tabbar3_250628 tabbar4_250628

ソースコード(全文)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.light(),
        scaffoldBackgroundColor: Colors.white,
        appBarTheme: AppBarTheme(
          backgroundColor: Colors.blue,
          foregroundColor: Colors.white,
        ),
        tabBarTheme: TabBarThemeData(
          labelColor: Colors.blue, // 選択されたタブの色
          unselectedLabelColor: Colors.grey, // 選択されていないタブの色
          //indicatorColor: Colors.blue, // indicatorで設定するので不要
          indicatorSize: TabBarIndicatorSize.tab,
          indicator: UnderlineTabIndicator(
            borderSide: BorderSide(color: Colors.blue, width: 2.0),
            insets: EdgeInsets.symmetric(horizontal: 8.0),
          ),
          dividerColor: const Color.fromRGBO(231, 231, 231, 1),
          dividerHeight: 1,
        ),
      ),
      home: TabScreen(),
    );
  }
}

enum TabScreenTabType {
  home,
  search,
  profile;

  static TabScreenTabType fromTabIndex(int index) {
    if (index < 0 || index > values.length - 1) {
      throw ArgumentError('Invalid tab index: $index');
    }
    return values[index];
  }

  String get title {
    switch (this) {
      case TabScreenTabType.home:
        return 'ホーム';
      case TabScreenTabType.search:
        return '検索';
      case TabScreenTabType.profile:
        return 'プロフィール';
    }
  }
}

class TabScreen extends StatefulWidget {
  const TabScreen({super.key});

  
  State<TabScreen> createState() => _TabScreenState();
}

class _TabScreenState extends State<TabScreen> with TickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _setUpTabController();
  }

  void _setUpTabController() {
    _tabController = TabController(
      length: TabScreenTabType.values.length,
      vsync: this,
    );

    _tabController.addListener(() {
      if (_tabController.indexIsChanging) {
        // タブの切り替えが始まったとき
        // タブをタップしたときのみ呼ばれる
        print(
          'タブがこれから切り替わります: index ${_tabController.previousIndex} -> ${_tabController.index}',
        );
      } else {
        // タブの切り替え完了。
        // タブをタップしたときも、スワイプで切り替えた場合も呼ばれる
        print(
          'タブが切り替わりました: ${TabScreenTabType.fromTabIndex(_tabController.index)}, 現在のindex: ${_tabController.index}. 前のindex ${_tabController.previousIndex}',
        );
      }
    });
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(context),
      body: _buildTabBarView(context),
    );
  }

  AppBar _buildAppBar(BuildContext context) {
    return AppBar(
      title: Text('タブ画面'),
      bottom: PreferredSize(
        preferredSize: Size.fromHeight(44),
        child: Container(
          color: Colors.white, // タブの背景色
          child: TabBar(
            controller: _tabController,
            onTap: (index) {
              print('タブがタップされたよ: $index');
            },
            tabs: [
              // アイコンバージョン
              // Tab(icon: Icon(Icons.home)),
              // Tab(icon: Icon(Icons.search)),
              // Tab(icon: Icon(Icons.person)),

              // タイトルバージョン
              Tab(text: TabScreenTabType.home.title),
              Tab(text: TabScreenTabType.search.title),
              Tab(text: TabScreenTabType.profile.title),
            ],
          ),
        ),
      ),
    );
  }

  TabBarView _buildTabBarView(BuildContext context) {
    return TabBarView(
      controller: _tabController,
      children: [
        Center(child: Text('ホーム画面')),
        Center(child: Text('検索画面')),
        Center(child: Text('プロフィール画面')),
      ],
    );
  }
}

説明

見た目の調整

全体的な見た目はThemeDataに設定してあります。

        tabBarTheme: TabBarThemeData(
          labelColor: Colors.blue, // 選択されたタブの色
          unselectedLabelColor: Colors.grey, // 選択されていないタブの色
          //indicatorColor: Colors.blue, // indicatorで設定するので不要
          indicatorSize: TabBarIndicatorSize.tab,
          indicator: UnderlineTabIndicator(
            borderSide: BorderSide(color: Colors.blue, width: 2.0),
            insets: EdgeInsets.symmetric(horizontal: 8.0),
          ),
          dividerColor: const Color.fromRGBO(231, 231, 231, 1),
          dividerHeight: 1,
        ),

高さを変更したかったのでPreferredSizeを使いました。
また背景色をナビゲーションヘッダーと違う色にしたかったのでContainerでTabBarを囲っています

  AppBar _buildAppBar(BuildContext context) {
    return AppBar(
      title: Text('タブ画面'),
      bottom: PreferredSize(
        preferredSize: Size.fromHeight(44),
        child: Container(
          color: Colors.white, // タブの背景色
          child: TabBar(
            controller: _tabController,
            onTap: (index) {
              print('タブがタップされたよ: $index');
            },
            tabs: [
              // アイコンバージョン
              // Tab(icon: Icon(Icons.home)),
              // Tab(icon: Icon(Icons.search)),
              // Tab(icon: Icon(Icons.person)),

              // タイトルバージョン
              Tab(text: TabScreenTabType.home.title),
              Tab(text: TabScreenTabType.search.title),
              Tab(text: TabScreenTabType.profile.title),
            ],
          ),
        ),
      ),
    );
  }

タブ切り替わりイベントの制御

タブ切り替わりイベントの制御をしたかったので、TabControllerを使っています。
メモリリークがおきないように、disposeでTabControllerを開放しています。

class _TabScreenState extends State<TabScreen> with TickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _setUpTabController();
  }

  void _setUpTabController() {
    _tabController = TabController(
      length: TabScreenTabType.values.length,
      vsync: this,
    );

    _tabController.addListener(() {
      if (_tabController.indexIsChanging) {
        // タブの切り替えが始まったとき
        // タブをタップしたときのみ呼ばれる
        print(
          'タブがこれから切り替わります: index ${_tabController.previousIndex} -> ${_tabController.index}',
        );
      } else {
        // タブの切り替え完了。
        // タブをタップしたときも、スワイプで切り替えた場合も呼ばれる
        print(
          'タブが切り替わりました: ${TabScreenTabType.fromTabIndex(_tabController.index)}, 現在のindex: ${_tabController.index}. 前のindex ${_tabController.previousIndex}',
        );
      }
    });
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

実際にタブをタップして、タブがきりかわるときのイベントのprint文です

I/flutter (27639): タブがこれから切り替わります: index 1 -> 0
I/flutter (27639): タブがタップされたよ: 0
I/flutter (27639): タブが切り替わりました: TabScreenTabType.home, 現在のindex: 0. 前のindex 1

コンテンツ部分を横にスワイプしてタブを切り替えた場合は、_tabController.indexIsChangingがfalseのときのみ呼ばれます

I/flutter (27639): タブが切り替わりました: TabScreenTabType.search, 現在のindex: 1. 前のindex 0

感想

細かいところの制御ができて良かったです。
今後は今回の上タブにgo_routerと下タブ(BottomNaigationBar)をからめたサンプルを作りたいと思っています

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.