[Flutter]BottomSheet内で画面遷移をする

## はじめに

ショッピングサイト等でよくある、商品の絞り込み毎に左→右へ画面遷移を繰り返すあのUIを、BottomSheetで実現したい。

イメージ参考：zozotownアプリ カテゴリー検索

![スクリーンショット 2024-01-04 21.33.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661825/21253ea0-09cb-b6ba-aeb7-2a51846b8bb9.png)



## 完成図

![キャプチャ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661825/3b94a9e8-5200-65d1-bb38-3f86d5b5e10b.gif)

① フローティングボタンタップで[絞り込み画面]表示
② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く
③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る
④ [カテゴリー画面]の×ボタンタップでシート自体pop

## BottomSheetを表示する

以下より実装方法を記載していきます。
まず① フローティングボタンタップで[絞り込み画面]表示　の処理
BottomSheetをPushします。

````dart

  Future<void> _incrementCounter() async {
    await Navigator.of(context).push(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        barrierColor: Colors.black.withOpacity(0.6),
        pageBuilder: (_, __, ___) => const BottomSheetView(),
        transitionDuration: const Duration(milliseconds: 500),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const Offset begin = Offset(0.0, 1.0); // 下から上にSheetを表示
          const Offset end = Offset.zero;
          final Animatable<Offset> tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.easeInOut));
          final Animation<Offset> offsetAnimation = animation.drive(tween);
          return SlideTransition(
            position: offsetAnimation,
            child: child,
          );
        },
      ),
    );
  }
````

② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く　の処理

ここで[カテゴリー画面]をpush時にboolを引数で返せるように設定する`await Navigator.of(context).push<bool?>`
③④でのpop処理時に`true`or`false`を渡すことでbottomSheet自体を閉じるか、一つ前の画面に戻るかを判定できる。

もちろん、`bool`以外を返却地に設定し、pop処理時に任意の値を渡すことも可能


````dart

  Future<void> _onTapCategoryButton() async {
    final backButton = await Navigator.of(context).push<bool?>(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        pageBuilder: (context, animation, secondaryAnimation) =>
            const CategoryBottomSheetView(),
      ),
    );
    if (backButton == null || backButton == false) {
      if (mounted) Navigator.of(context).pop();
    }
  }
}
````

③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る の処理

````dart
  void _onPressedBackButton() {
    Navigator.of(context).pop(true);
  }
````

④ [カテゴリー画面]の×ボタンタップでシート自体pop

````dart
  void _onPressedCloseButton() {
    Navigator.of(context).pop(false);
  }
````

## コード全体

`main.dart`
````dart
import 'package:flutter/material.dart';
import 'package:test_project/bottomsheet_view.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Container(),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.egg_alt_sharp),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Future<void> _incrementCounter() async {
    await Navigator.of(context).push(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        barrierColor: Colors.black.withOpacity(0.6),
        pageBuilder: (_, __, ___) => const BottomSheetView(),
        transitionDuration: const Duration(milliseconds: 500),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const Offset begin = Offset(0.0, 1.0); // 下から上
          const Offset end = Offset.zero;
          final Animatable<Offset> tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.easeInOut));
          final Animation<Offset> offsetAnimation = animation.drive(tween);
          return SlideTransition(
            position: offsetAnimation,
            child: child,
          );
        },
      ),
    );
  }
}

````

`bottomsheet_view.dart`
````dart
import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:test_project/categorybottomsheet_view.dart';

class BottomSheetView extends StatefulWidget {
  const BottomSheetView({Key? key}) : super(key: key);

  @override
  _BottomSheetView createState() => _BottomSheetView();
}

class _BottomSheetView extends State<BottomSheetView> {
  _BottomSheetView() {}

  @override
  Widget build(final BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.only(
              topRight: Radius.circular(12), topLeft: Radius.circular(12)),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3), // changes position of shadow
            ),
          ],
        ),
        height: MediaQuery.of(context).size.height * 0.8,
        width: double.maxFinite,
        child: Material(
          type: MaterialType.transparency,
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                //余白
                const Divider(color: Colors.transparent),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.transparent,
                      onPressed: () {},
                    ),
                    const Padding(
                      padding: EdgeInsets.symmetric(horizontal: 9.0),
                      child: AutoSizeText("絞り込み"),
                    ),
                    //余白
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.transparent,
                      onPressed: () {},
                    ),
                  ],
                ),
                ListTile(
                  onTap: _onTapCategoryButton,
                  title: const Text(
                    "カテゴリー",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),
                ListTile(
                  onTap: () {
                    // _onTapCreateReadyMadeClubButton(GolfclubCategoryType.hybridUtility);
                  },
                  title: const Text(
                    "ショップ",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),
                ListTile(
                  onTap: () {
                    // _onTapCreateReadyMadeClubButton(GolfclubCategoryType.iron);
                  },
                  title: const Text(
                    "オプション",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),

                const Divider(color: Colors.transparent),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  Future<void> _onTapCategoryButton() async {
    final backButton = await Navigator.of(context).push<bool?>(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        pageBuilder: (context, animation, secondaryAnimation) =>
            const CategoryBottomSheetView(),
      ),
    );
    if (backButton == null || backButton == false) {
      if (mounted) Navigator.of(context).pop();
    }
  }
}

````

`categorybottomsheet_view.dart`
````dart
import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';

class CategoryBottomSheetView extends StatefulWidget {
  const CategoryBottomSheetView({Key? key}) : super(key: key);

  @override
  _CategoryBottomSheetView createState() => _CategoryBottomSheetView();
}

class _CategoryBottomSheetView extends State<CategoryBottomSheetView> {
  _CategoryBottomSheetView() {}

  @override
  Widget build(final BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.only(
              topRight: Radius.circular(12), topLeft: Radius.circular(12)),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3), // changes position of shadow
            ),
          ],
        ),
        height: MediaQuery.of(context).size.height * 0.8,
        width: double.maxFinite,
        child: Material(
          type: MaterialType.transparency,
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                //余白
                const Divider(color: Colors.transparent),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.black,
                      onPressed: _onPressedBackButton,
                    ),
                    const Padding(
                      padding: EdgeInsets.symmetric(horizontal: 9.0),
                      child: AutoSizeText("カテゴリー"),
                    ),
                    //中央揃えのため透明アイコンを配置
                    IconButton(
                      icon: const Icon(
                        Icons.close,
                        size: 21,
                      ),
                      color: Colors.black,
                      onPressed: _onPressedCloseButton,
                    ),
                  ],
                ),
                const ListTile(
                  onTap: null,
                  title: Text(
                    "トップス",
                    style: TextStyle(fontSize: 15),
                  ),
                ),
                ListTile(
                  onTap: () {},
                  title: const Text(
                    "ボトムス",
                    style: TextStyle(fontSize: 15),
                  ),
                ),
                const Divider(color: Colors.transparent),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  void _onPressedBackButton() {
    Navigator.of(context).pop(true);
  }

  void _onPressedCloseButton() {
    Navigator.of(context).pop(false);
  }
}

````

## 参考

https://qiita.com/superman9387/items/0025d23861c6c076e867