[Flutter] BottomSheet(ボトムシート)の表示とチップス

BottomSheetは、画面下から伸びてきて、何らかの入力を求めるWidgetです。私が実際にBottomSheetを使用してみて、疑問に思って検索して分かったことを汎用性の高い形式のソースを作成してまとめています(デザイン的には、アレですが)。

この記事で分かることは、以下の通りです。

基本的な表示方法とボトムシートのサイズ(長さ?)の指定、結果の使い方

void _onTap() async {
  final result = await showModalBottomSheet<int>(
    context: context,
    builder: (context) => SizedBox(
        height: 200,
        child: Container(),
    )
  );
  if (result != null) {
    setState(() => _counter += result);
  }
}

ボトムシートは、showModelBottomSheetで表示させます。この関数をボタンを押したときなどのイベント内に書きます。結果として返したい型をジェネリックで指定します(ここでは、intを指定)。
ボトムシートの中身は、builderの中身に書きます。Widgetを指定するので、自由には設定できます。
ボトムシートを指定しないと大きくなりがちなので、SizeBoxを使うことで、長さを指定しています。

ボトムシートから結果を受け取ることができ、非同期でボトムシートが閉じられるのを待ちます。ボトムシートが閉じられると結果が返ってくるので受け取ります(この場合、result)。結果の値を見て、処理を実施します。

ボトムシートの閉じ方と結果の返し方

Navigator.of(context).pop()  // 結果はnull
Navigator.of(context).pop(2) // 結果は2

ボトムシート以外の箇所を押すと、ボトムシートは消えます。そのときの結果はnullになります。
プログラム側でボトムシートを消したい場合は、Navigator.of(context).pop()を使います。popの引数に結果を設定し、引数無しならnull、設定すればその値が結果として戻されます。

ボトムシートの上部を角円にする方法

showModalBottomSheet<int>(
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(25.0)),
),

デフォルトのままでは、ボトムシートの上部が四角になります。デザイン的に角円の方が良いかと思いますので、上記を設定します。

ボトムシート内での状態変化の反映方法

StatefulBuilder(
  builder:  (BuildContext context, Function setSheetState) {
    return Column(
      children: [
        Checkbox(
          value: _isChecked,
          onChanged: (value) =>
             setSheetState(() => _isChecked = value ?? false),
        ),
      ],
    );
  },
),

setStateを使っても、ボトムシートを開いたページのsetStateが呼ばれているので、ボトムシート内の状態変化には対応できませんでした(この例ですと、チェックボックスをチェックしても、チェックされた状態にならない)。StatefulBuilderというWidgetを使うことで、解決できました。
StatefulBuilderを使用すると、引数に該当のWidgetのsetState的な関数を取得できるようです。ただ、setStateという名前にすると元のページのsetStateが隠されて使えないので、名前をsetSheetStateに変えてみました。
こうすることで、チェックボックスにチェックを入れたり、ラジオボタンで初期値以外のボタンを選べるようになりました。

全ソース

ということで、上記の全てを入れたテストコードを記載しておきます。ボトムシート内でチェックボックスのチェックを押すと、チェックがされて、ボタンの表示が変わります。そして、結果がチェックなしだと1,、ありだと2が戻され、_counterに足されます

また一番上のボタンは、結果NULLを返すようになってます。
また、ボトムシートの長さが指定されていて、角円になっています。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  bool _isChecked = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _onTap,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  void _onTap() async {
    final result = await showModalBottomSheet<int>(
      context: context,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(25.0)),
      ),
      builder: (context) => SizedBox(
        height: 200,
        child: StatefulBuilder(
          builder: (BuildContext context, Function setSheetState) {
            return Column(
              children: [
                OutlinedButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text('Nullを返す')),
                Checkbox(
                  value: _isChecked,
                  onChanged: (value) =>
                      setSheetState(() => _isChecked = value ?? false),
                ),
                OutlinedButton(
                    onPressed: () =>
                        Navigator.of(context).pop(_isChecked ? 2 : 1),
                    child: Text('${_isChecked ? 2 : 1}を返す')),
              ],
            );
          },
        ),
      ),
    );
    print('result: $result');
    if (result != null) {
      setState(() => _counter += result);
    }
  }
}

まとめ

Flutterでのボトムシートの基本的な表示方法から、状態変化、角円にする方法などを紹介しました。
「Flutter bottomSheet」で検索しても、表示方法しか記載されていないページが多かったので、ボトムシートを舐めてかかってました。しかし、実際に使ってみると、状態変化の反映など、なかなか手強かったです。通常はボトムシートで色々と入力させないんですかね。

参考