🔖

go_routerでタブがある状態で画面遷移

2022/12/03に公開

Flutterのgo_routerのルーティングライブラリでタブがある状態で画面遷移を行う実装方法です。
Flutterのversionは3.3.8
go_routerのversionは5.2.1
です。

機能

  • タブを維持しながら、画面遷移
  • 同じ画面を別々のタブで表示
  • タブがない画面を表示

画像が荒いですが、gifです

ポント

  • タブの維持はShellRouteで定義
  • 子遷移はrouteを入れ子で定義
  • タブのアクティブはURLの前方一致で定義

遷移でgoメソッドを使うとスタックされた状態で遷移。なので、詳細→一覧→Topとなります。
pushでやると直接詳細画面へ遷移し、TOPに戻る

実装手順

  1. タブ(ScaffoldWithNavBar)を作成。(tab.dart)
      ここでどのタブがアクティブにするか、タブを押したらどこに遷移するかを定義。
  2. 各画面を作成。
  3. ルートの定義で同一タブの遷移をShellRouteでまとめる(router.dart)
  4. タブを使わないルートはShellRouteで囲まない
  5. 子遷移は入れ子で定義

タブの実装部分

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

class ScaffoldWithNavBar extends StatelessWidget {
  /// Constructs an [ScaffoldWithNavBar].
  const ScaffoldWithNavBar({
    required this.child,
    Key? key,
  }) : super(key: key);

  final Widget child;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'ホーム',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.business),
            label: '記事',
          ),
        ],
        currentIndex: _calculateSelectedIndex(context),
        onTap: (int idx) => _onItemTapped(idx, context),
      ),
    );
  }

  static int _calculateSelectedIndex(BuildContext context) {
    final String location = GoRouterState.of(context).location;
    if (location.startsWith('/home')) {
      return 0;
    }
    if (location.startsWith('/article')) {
      return 1;
    }
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0:
        GoRouter.of(context).go('/');
        break;
      case 1:
        GoRouter.of(context).go('/article/list');
        break;
    }
  }
}


ルーティング部分

router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import '../main.dart';
import '../presentation/article_detail/article_detail_page.dart';
import '../presentation/article_list/artilce_list_page.dart';
import '../presentation/common/tab.dart';
import '../presentation/detail/detail_page.dart';
import '../presentation/list/list_page.dart';
import '../presentation/rule/rule_page.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'shell');

// GoRouter configuration
final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/',
  routes: [
    //タブのない画面
    GoRoute(path: '/rule', builder: (context, state) => const RulePage()),
    //タブありの画面
    ShellRoute(
        navigatorKey: _shellNavigatorKey,
        builder: (BuildContext context, GoRouterState state, Widget child) {
          return ScaffoldWithNavBar(child: child);
        },
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (context, state) => const MyHomePage(title: "トップページ"),
            routes: <RouteBase>[
              GoRoute(
                path: 'home/list',
                builder: (BuildContext context, GoRouterState state) {
                  return const ListPage();
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'detail',
                    builder: (BuildContext context, GoRouterState state) {
                      return const DetailPage();
                    },
                  )
                ],
              ),
              GoRoute(
                path: 'article/list',
                builder: (BuildContext context, GoRouterState state) {
                  return const ArticleListPage();
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'detail',
                    builder: (BuildContext context, GoRouterState state) {
                      return const ArticleDetailPage();
                    },
                  ),
                  GoRoute(
                    path: 'adetail',
                    builder: (BuildContext context, GoRouterState state) {
                      return const DetailPage();
                    },
                  )
                ],
              )
            ],
          ),
        ]),
  ],
);

トップ画面

main.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:tab_app/presentation/common/tab.dart';
import 'router/router.dart';

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

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

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '遷移のサンプル',
            ),
            ElevatedButton(
              child: const Text('一覧へ'),
              onPressed: () {
                context.go('/home/list');
              },
            ),
            ElevatedButton(
              child: const Text('pushで詳細へ'),
              onPressed: () {
                context.push('/home/list/detail');
              },
            ),
            ElevatedButton(
              child: const Text('goで詳細へ'),
              onPressed: () {
                context.go('/home/list/detail');
              },
            ),
            ElevatedButton(
              child: const Text('記事一覧へ'),
              onPressed: () {
                context.go('/article/list');
              },
            ),
            ElevatedButton(
              child: const Text('記事詳細へ'),
              onPressed: () {
                context.go('/article/list/detail');
              },
            ),
            ElevatedButton(
              child: const Text('利用規約へ'),
              onPressed: () {
                context.go('/rule');
              },
            ),
          ],
        ),
      ),
    );
  }
}

コードはGitHubに全てあります。
https://github.com/Khanashima/flutter-tab-base

基本、公式サイトのコードを真似ています
https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/shell_route.dart

Discussion