💬

【Flutter Web】 go_routerでのタブ実装(タブとURLパスを連動して切り替える) 

2023/01/07に公開

はじめに

はじめまして、ヤスーです。
普段はメディア系のSIer、趣味でFlutterアプリ開発を行なっています。

今回はFlutter Webのタブ実装で、タブを切り替えた際にURLパスも連動して切り替える実装を記事にしてみました。
通常のモバイルアプリでは、タブの画面遷移でURLを意識する必要はありません。しかし、Webになるとタブを切り替えた際にURLも変更して画面遷移したい場面が出てくるので、その方法を載せています。
画面遷移はgo_routerを用いた実装になっています。
サンプルコードは以下に格納していますので、ぜひこちらも合わせて参考にしていただけると幸いです。

https://github.com/yasui-kohei/tab_for_flutter_web

目次

  1. パッケージのインストール
  2. Flutter Webにおけるタブの実装
  3. おわりに

1. パッケージのインストール

画面遷移で必要になるgo_routerをインストールしていきます。
Flutterプロジェクトのpubspec.yamlに、go_routerを追加してpub getします。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
+ go_router: ^6.0.1

2. Flutter Webにおけるタブの実装

今回は3つのタブを用意します。
各タブは「/myPage/tab1」「/myPage/tab2」「/myPage/tab3」のURLパスを持つようにして、タブを切り替えるとURLと画面が切り替わるような実装をおこないます。
※タブの実装と関係ない部分は省いて説明しますので、ご了承ください。

2.1 各タブで表示する画面を作成

各タブで表示する画面ページを用意する。

tab.dart
import 'package:flutter/material.dart';

///[/myPage/tab1]のパスで表示する画面
class Tab1Page extends StatelessWidget {
  const Tab1Page({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Center(child: Text('1'));
  }
}

///[/myPage/tab2]のパスで表示する画面
class Tab2Page extends StatelessWidget {
  const Tab2Page({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Center(child: Text('2'));
  }
}

///[/myPage/tab3]のパスで表示する画面
class Tab3Page extends StatelessWidget {
  const Tab3Page({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const Center(child: Text('3'));
  }
}

2.2 タブ情報をリストデータで保持するクラスを作成

tab_items.dartに以下のようなタブに必要な情報を書く。

tab_items.dart
import 'package:flutter/material.dart';
import 'package:flutter_web_tab/tab.dart';

/// Tabの文字とクリックされた時表示するviewを格納するデータクラス
class TabItem {
  const TabItem({required this.tabId, required this.tab, required this.view});

  /// タブ固有のID
  final String tabId;

  /// 各タブのウィジェット
  final Tab tab;

  /// 各タブで表示する画面ページ
  final Widget view;
}

/// MyPageのタブのリストデータ用クラス
class MyPageTabs {
  static final data = [
    // tab1のタブ情報
    const TabItem(
        tabId: "tab1",
        tab: Tab(
          child: Text(
            'タブ1',
          ),
        ),
        view: Tab1Page()),
    // tab2のタブ情報
    const TabItem(
      tabId: "tab2",
      tab: Tab(
        child: Text(
          'タブ2',
        ),
      ),
      view: Tab2Page(),
    ),
    // tab3のタブ情報
    const TabItem(
      tabId: "tab3",
      tab: Tab(
        child: Text(
          'タブ3',
        ),
      ),
      view: Tab3Page(),
    ),
  ];
}

TabItemクラスは以下のフィールド要素を持つ。
tabId:タブの固有ID
tab:各タブのウィジェット
view:各タブで表示する画面ウィジェット

MyPageTabsクラスはTabItemをリストで保持する。
tabviewは以下の画像で示す部分を表す。

2.3 タブのウィジェットを作成

tab_widget.dartを作成して、タブ切り替えができるウィジェットを以下のように作成します。

tab_widget.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:sukimachi/presentation/my_page/tabs/tab_item.dart';
import 'package:sukimachi/presentation/my_page/tabs/my_page_tabs_component.dart';

/// MyPageの表示情報を切り替えるタブ
///
/// 依頼情報や受注情報のみに切り替えるために使用する。
class MyPageBodyTab extends StatefulWidget {
  const MyPageBodyTab({Key? key, required this.currentTab, required this.index})
      : super(key: key);
  final TabItem currentTab;
  final int index;

  
  _MyPageBodyTabState createState() => _MyPageBodyTabState();
}

class _MyPageBodyTabState extends State<MyPageBodyTab>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _tabController = TabController(
      length: MyPageTabs.data.length,
      vsync: this,
      initialIndex: widget.index,
    );
  }

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

  
  void didUpdateWidget(MyPageBodyTab oldWidget) {
    super.didUpdateWidget(oldWidget);
    _tabController.index = widget.index;
  }

  
  Widget build(BuildContext context) {
    return ListView(
      shrinkWrap: true,
      children: [
        Center(
          child: TabBar(
            controller: _tabController,
            indicatorColor: Colors.black,
            indicatorSize: TabBarIndicatorSize.label,
            labelColor: Colors.black,
            unselectedLabelColor: Colors.grey.withOpacity(0.6),
            isScrollable: true,
            onTap: (int index) {
              _onTap(context, index);
            },
            tabs: [for (TabItem t in MyPageTabs.data) t.tab],
          ),
        ),
        // tabのボディ
        IndexedStack(
          alignment: Alignment.center,
          index: widget.index,
          children: [
            for (final t in MyPageTabs.data)
              Visibility(
                  child: t.view,
                  visible: widget.index ==
                      MyPageTabs.data
                          .indexWhere((tab) => tab.tabId == t.tabId)),
          ],
        )
      ],
    );
  }

  void _onTap(BuildContext context, int index) {
    context.go('/myPage/${MyPageTabs.data[index].tabId}');
  }
}

以下はtab_widget.dartの解説になります。

initStateで、TabControllerを作成します。

tab_widget.dart
  
  void initState() {
    super.initState();
    _tabController = TabController(
      length: MyPageTabs.data.length,
      vsync: this,
      initialIndex: widget.index,
    );
  }

didUpdateWidgetは、親のウィジェットが再構築される時にコール関数になります。
この際に、tabController.indexをURLのパスで指定したタブ番号に随時変更してあげることで、タブ毎に表示する画面を変更しています。

  @override
  void didUpdateWidget(MyPageBodyTab oldWidget) {
    super.didUpdateWidget(oldWidget);
    _tabController.index = widget.index;
  }

Widgetはスクロール可能なタブ画面とするためにListViewで囲っています。
また、URLのパスで指定されたウィジェットのみをvisibleで設定することでタブの切り替えを実現しています。

  @override
  Widget build(BuildContext context) {
    return ListView(
      shrinkWrap: true,
      children: [
        Center(
          child: TabBar(
            controller: _tabController,
            indicatorColor: Colors.black,
            indicatorSize: TabBarIndicatorSize.label,
            labelColor: Colors.black,
            unselectedLabelColor: Colors.grey.withOpacity(0.6),
            isScrollable: true,
            onTap: (int index) {
              _onTap(context, index);
            },
            tabs: [for (TabItem t in MyPageTabs.data) t.tab],
          ),
        ),
        // tabのボディ
        IndexedStack(
          alignment: Alignment.center,
          index: widget.index,
          children: [
            for (final t in MyPageTabs.data)
              Visibility(
                  child: t.view,
                  visible: widget.index ==
                      MyPageTabs.data
                          .indexWhere((tab) => tab.tabId == t.tabId)),
          ],
        )
      ],
    );
  }

2.4 タブウィジェットを配置する画面を作成

今回はMyPageにタブ切り替えのウィジェットを導入する。

my_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_web_tab/tab_items.dart';
+ import 'package:flutter_web_tab/tab_widget.dart';

/// マイページ画面
class MyPage extends StatelessWidget {
  const MyPage({Key? key, required this.currentTab, required this.index})
      : super(key: key);
  final TabItem currentTab;
  final int index;

  
  Widget build(BuildContext context) {
    return Scaffold(
 +     body: MyPageTabWidget(
 +       currentTab: currentTab,
 +       index: index,
 +     ),
    );
  }
}

MyPageの引数には、現在表示しているタブ情報とタブの番号(何番目のタブであるかの情報)を設定して、外部から受け取れるようにする。これは、次のgo_routerの設定で外部から受け取るのに必要である。

2.5 go_routerでタブ切り替えするための画面遷移設定

router.dartに以下を追記する。

router.dart
import 'package:flutter/material.dart';
import 'package:flutter_web_tab/home_page.dart';
import 'package:go_router/go_router.dart';
+ import 'package:flutter_web_tab/my_page.dart';
+ import 'package:flutter_web_tab/tab_items.dart';

/// 画面遷移の情報を定義する
final router = GoRouter(
  initialLocation: "/",
  routes: [
    GoRoute(
      path: "/",
      pageBuilder: (context, state) => MaterialPage(
        key: state.pageKey,
        child: const HomePage(),
      ),
    ),

+    ///MyPageのタブに表示される情報の切り替えを行うためのGoRouteの設定
+    //[/myPage/1],[/myPage/2],[/myPage/3]のパスに切り替える。
+    //1,2,3はmyPageIdを示す。
+    GoRoute(
+        name: 'myPage',
+        path: '/myPage/:myPageId"',
+        pageBuilder: (context, state) {
+          final myPageId = state.params['myPageId'];
+          final currentTab =
+              MyPageTabs.data.firstWhere((tab) => tab.tabId == myPageId);
+          final index =
+              MyPageTabs.data.indexWhere((t) => t.tabId == currentTab.tabId);
+          return MaterialPage(
+            key: state.pageKey,
+            child: MyPage(
+              currentTab: currentTab,
+              index: index,
+            ),
+          );
+        }),
  ],
);

currentTabは、URLパスのmyPageIdと一致するIDを持ったTabItemが格納されています。
TabItemはtab_items.dartのMyPageTabsに格納されている情報になります。
また、indexはタブ順の番号が格納されている。

3. おわりに

最後までお読みいただきありがとうございました。
go_routerを用いたBottomNavigationBarの画面遷移実装の記事は幾つかあったのですが、Webでのタブ切り替えの記事はあまりみたことがなかったので書いてみた次第です。
参考になれば幸いです。

参考

https://pub.dev/packages/go_router/install
https://pub.dev/documentation/go_router/latest/topics/Get started-topic.html
https://stackoverflow.com/questions/52023610/getting-horizontal-viewport-was-given-unbounded-height-with-tabbarview-in-flu
https://qiita.com/kurun_pan/items/116288b8ab2c409d2ee5

Discussion