【Flutter】go_router、auto_routeで宣言的にダイアログやボトムシートをオープンする

2024/03/03に公開

1. はじめに

Flutterでダイアログやボトムシート表示する場合、以下のfunctionを使用すると思います。

など

ちょっとした通知のダイアログ等であれば命令的に表示する実装で良いと思うのですが、アプリの規模が大きくなったりダイアログやボトムシートがメインコンテンツになってくると、宣言的にルーティングしたくなると思います。
そこで、こちらの記事を参考にしつつ、go_routerにて宣言的にダイアログとボトムシートをオープンしてみようと思います。

https://croxx5f.hashnode.dev/adding-modal-routes-to-your-gorouter

2. 挙動

以下のような挙動になります。URLの変化も確認できると思います。

go_router

3. コード

使用したパッケージは、go_routerのみです。

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

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: routerConfig,
      theme: ThemeData(
        bottomSheetTheme: const BottomSheetThemeData(
          surfaceTintColor: Colors.transparent,
        ),
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: const Text('open dialog'),
              onPressed: () => context.goNamed(RoutingConfig.dialog.name),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              child: const Text('open bottom sheet'),
              onPressed: () => context.goNamed(RoutingConfig.bottomSheet.name),
            ),
          ],
        ),
      ),
    );
  }
}

final routerConfig = GoRouter(
  routes: [
    GoRoute(
      path: RoutingConfig.home.path,
      name: RoutingConfig.home.name,
      pageBuilder: (_, __) => const MaterialPage(child: Home()),
      routes: [
        GoRoute(
          path: RoutingConfig.dialog.path,
          name: RoutingConfig.dialog.name,
          pageBuilder: (_, __) => DialogPage(
            builder: (_) => const SampleContent(),
          ),
        ),
        GoRoute(
          path: RoutingConfig.bottomSheet.path,
          name: RoutingConfig.bottomSheet.name,
          pageBuilder: (_, __) => BottomSheetPage(
            builder: (_) => const SampleContent(),
          ),
        ),
      ],
    ),
  ],
);

enum RoutingConfig {
  home('/', 'home'),
  dialog('dialog', 'dialog'),
  bottomSheet('bottom-sheet', 'bottomSheet');

  final String path;
  final String name;

  const RoutingConfig(this.path, this.name);
}

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

  
  Widget build(BuildContext context) {
    return AlertDialog(
      backgroundColor: Colors.white,
      surfaceTintColor: Colors.transparent,
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text('Sample Content'),
          const SizedBox(height: 12),
          ElevatedButton(
            onPressed: context.pop,
            child: const Text('close'),
          ),
        ],
      ),
    );
  }
}

class DialogPage<T> extends Page<T> {
  final Offset? anchorPoint;
  final Color? barrierColor;
  final bool barrierDismissible;
  final String? barrierLabel;
  final bool useSafeArea;
  final CapturedThemes? themes;
  final WidgetBuilder builder;

  const DialogPage({
    required this.builder,
    this.anchorPoint,
    this.barrierColor = Colors.black54,
    this.barrierDismissible = true,
    this.barrierLabel,
    this.useSafeArea = true,
    this.themes,
    super.key,
    super.name,
    super.arguments,
    super.restorationId,
  });

  
  Route<T> createRoute(BuildContext context) {
    return DialogRoute<T>(
      context: context,
      settings: this,
      builder: builder,
      anchorPoint: anchorPoint,
      barrierColor: barrierColor,
      barrierDismissible: barrierDismissible,
      barrierLabel: barrierLabel,
      useSafeArea: useSafeArea,
      themes: themes,
    );
  }
}

class BottomSheetPage<T> extends Page<T> {
  final WidgetBuilder builder;
  final Offset? anchorPoint;
  final String? barrierLabel;
  final CapturedThemes? themes;

  const BottomSheetPage({
    required this.builder,
    this.anchorPoint,
    this.barrierLabel,
    this.themes,
  });

  
  Route<T> createRoute(BuildContext context) {
    return ModalBottomSheetRoute(
      settings: this,
      builder: builder,
      anchorPoint: anchorPoint,
      barrierLabel: barrierLabel,
      isScrollControlled: true,
      backgroundColor: Colors.white,
      constraints: BoxConstraints(
        maxHeight: MediaQuery.sizeOf(context).height / 2,
      ),
      useSafeArea: true,
      showDragHandle: true,
      elevation: 1.0,
    );
  }
}

参考にさせて頂いたページに記載されているとおり、Pageクラスを継承してカスタムのダイアログページとモーダルシートページを用意しています。
そして、以下のようにpageBuilderの返却値として利用しています。

        GoRoute(
          path: RoutingConfig.dialog.path,
          name: RoutingConfig.dialog.name,
          pageBuilder: (_, __) => DialogPage(
            builder: (_) => const SampleContent(),
          ),
        ),
        GoRoute(
          path: RoutingConfig.bottomSheet.path,
          name: RoutingConfig.bottomSheet.name,
          pageBuilder: (_, __) => BottomSheetPage(
            builder: (_) => const SampleContent(),
          ),
        ),

ダイアログやボトムシートをオープンする際には、goNamedを使用しており(context.goでパスを使用しても良いですが)、showDialogshowModalBottomSheetは使用していません。

context.goNamed(RoutingConfig.dialog.name)
context.goNamed(RoutingConfig.bottomSheet.name)

4. おわりに

ダイアログやボトムシートを宣言的にオープンできるようになると、deep linkに対応できるようになると思います。
例えばFCMのメッセージを受けて該当するお知らせを表示するなど、外部からアプリの特定のダイアログやボトムシートを出したいようなケースに対応できるかなと思います。

5. 追記

上記のコードを元に、deeplinkの記事を記載しました。

https://zenn.dev/motu2119/articles/deep-linking-20240502

6. さらに追記 (auto_route)

auto_route でも同様のサンプルを用意しました。

https://github.com/motucraft/auto_route_playground/blob/main/lib/declarative_routing/main_declarative_routing.dart

7. さらにさらに追記

auto_route については、こちらの記事も記載しました。
https://zenn.dev/motu2119/articles/tab-navigation-with-auto-route-20241103

GitHubで編集を提案

Discussion