【Flutter】ダイアログの上に入力フォームを表示したい（overflowedエラー対応）

## はじめに
下記の実現イメージのようなアカウント情報を表示する画面と画面内の編集ボタンを押すと
ダイアログみたいに元の画面に重ねてアカウント編集画面を表示するものを作成していたのですが
その途中でエラーに悩まされたのでメモとして残します。

### 実装イメージ
| アカウント情報画面 | アカウント編集画面 |
|:-----------:|:------------:|
|<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580558/fff48bf3-2855-104c-b061-008b3df3fbc5.png">|<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580558/a4f7b87b-cd32-2076-aa1d-47123bb61fb2.png">|


## 開発環境
- MacOS: 12.4(Monterey)
- Androidstudio: 2021.2.1 patch1
- flutter: 3.3.4
- dart: 2.18.2

## 対応手順
#### (1)アカウント情報画面の作成
アカウント情報画面のデザインは任意で問題ないのですがその上にダイアログを乗せるため、
「編集ボタン」タップ時に「showDialog()」でアカウント情報編集画面を呼び出します。
（※全体のソースコードは最後に記載します。）
```dart:test_screen.dart
onPressed: () {
    // アカウント情報編集画面を表示する
    showDialog(
        context: context,
        builder: (context) {
          return DialogWidget().dialogSampleWidget(context),
          );
        });
},
```
#### (2)アカウント編集画面(ダイアログ)の作成
アカウント編集画面自体のデザインは任意のもので問題ないのですが、
ダイアログのような画面にするためにベースは「AlertDialog」を使用しました。
「AlertDialog」の子要素として入力フォーム（TextFormField）を追加しております。

```dart:dialog_widget.dart
class DialogWidget {
  Widget dialogSampleWidget(BuildContext context) {

    return AlertDialog(
      insetPadding: const EdgeInsets.symmetric(vertical: 30, horizontal: 10),
      content: Column(
        // ここでは省略 (入力フォーム(TextFormField)などはこちらに追加)
    );
}
```
#### (3)アカウント編集画面の入力フォームをタップすると画面が崩れる
これは馴染みがあるかもしれないのですが、
入力フォームを含むデザイン部分を実装していざ動作確認をしてみると画面に黄色と黒色のシマシマが表示されました。

原因は作成していたデザイン部分に予想していないキーボード分のエリアが加わったため
レイアウトが崩れるといった事象になります。

対応案の一つとして今回はキーボードのエリア分を自動的に画面スクロールさせるために
「SingleChildScrollView」でラップします。

この対応で入力フォームをタップするとキーボード分レイアウトが上にスクロールされるようになりました。
```dart:test_screen.dart
onPressed: () {
    // アカウント編集画面を表示する
    showDialog(
        context: context,
        builder: (context) {
          return SingleChildScrollView(// ここにスクロールを追加！！
            reverse: true,
            child:
                DialogWidget().dialogSampleWidget(context),
          );
        });
    },
```
| 修正前 | 修正後 |
|:-----------:|:------------:|
| <img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580558/2b48f2d6-6e53-6ff3-9171-e48972cb5380.png"> |　<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580558/6d146b4f-203c-a002-2543-29ae1ebe59b0.png">　|

#### (4)解決したかと思いきやまだエラーログが。。。
(3)の対応でアプリ画面上では問題なさそうに見えるのですがコンソールにはまだエラーログが・・・
ログを確認してみるとダイアログ側ではなく親ビュー「アカウント情報画面」の方でoverflowingが発生しておりました。。
「test_screen.dart:48:20」

どうやらViewを被せた「アカウント編集画面」上でキーボードを開くと
親となる「アカウント情報画面」にも影響がある模様。。

##### エラーログ
```
The following assertion was thrown during layout:
A RenderFlex overflowed by 191 pixels on the bottom.

The relevant error-causing widget was: 
  Column Column:file:///Documents/sample/lib/screen/test/view/test_screen.dart:48:20
The overflowing RenderFlex has an orientation of Axis.vertical.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and black striped pattern.
This is usually caused by the contents being too big for the RenderFlex.
```

#### (5)親ビューである「アカウント情報画面」のエラー対応

これは根本解決にはなっていないかもしれないですが「アカウント情報画面」のWidgetにも
「SingleChildScrollView」をラップすることで解消しました！！
（恐らくダイアログの下で画面がスクロールされていると思います。）

もし良い解決案があればご教示いただけると幸いです。

## ソースコード
対応内容で記載した対応後のソースコードとなります。
（サンプル用でさらっとコーディングしたので汚いですが。。すみません）

```dart:test_screen.dart
import 'package:sample/screen/test/view/dialog_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

///
/// アカウント情報画面
///
class TestScreen extends ConsumerWidget {
  const TestScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final deviceHeight = MediaQuery.of(context).size.height;
    const userInfoSidePadding = 30.0;

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text(
          "アカウント情報",
          style: TextStyle(color: Colors.black, fontSize: 20),
        ),
        leading: GestureDetector(
          onTap: () {
            Navigator.of(context, rootNavigator: false).pop();
          },
          child: const Icon(
            Icons.chevron_left,
            color: Colors.green,
          ),
        ),
        backgroundColor: Colors.white,
      ),
      backgroundColor: Colors.grey,
      body: Center(
        child: Container(
          margin: EdgeInsets.only(
              left: 20,
              top: deviceHeight / 15,
              right: 20,
              bottom: deviceHeight / 10),
          height: double.infinity,
          child: Card(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
            clipBehavior: Clip.antiAliasWithSaveLayer,
            child: SingleChildScrollView(// ここにスクロールを追加
              child: Column(
                children: [
                  Column(
                    children: [
                      Container(
                        padding: const EdgeInsets.only(
                            left: userInfoSidePadding,
                            top: 20,
                            right: userInfoSidePadding),
                        child:
                            _info(context, "アカウント", "sample_account@test.com"),
                      ),
                      const Divider(
                        color: Colors.grey,
                        height: 10,
                      ),
                      Container(
                        padding: const EdgeInsets.only(
                            left: userInfoSidePadding,
                            right: userInfoSidePadding),
                        child: _info(context, "名前", "サンプル　太郎"),
                      ),
                      const Divider(
                        color: Colors.grey,
                        height: 10,
                      ),
                      Container(
                        padding: const EdgeInsets.only(
                            left: userInfoSidePadding,
                            right: userInfoSidePadding),
                        child: _info(context, "職業", "なし"),
                      ),
                    ],
                  ),
                  SizedBox(height: deviceHeight / 10),
                  Container(
                    width: double.infinity,
                    margin:
                        const EdgeInsets.only(left: 20, right: 20, bottom: 30),
                    padding: const EdgeInsets.only(top: 5, bottom: 5),
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor: Colors.green,
                      ),
                      onPressed: () {
                        // アカウント編集画面を表示する
                        showDialog(
                            context: context,
                            builder: (context) {
                              return SingleChildScrollView(// ここにスクロールを追加
                                reverse: true,
                                child:
                                    DialogWidget().dialogSampleWidget(context),
                              );
                            });
                      },
                      child: const Text("編集"),
                    ),
                  )
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _info(BuildContext context, String title, String value) {
    return Container(
        margin: const EdgeInsets.only(top: 15, bottom: 15),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            FittedBox(
              child: Row(
                children: [
                  Container(
                    height: 25,
                    width: 5,
                    decoration: BoxDecoration(
                      color: Colors.green,
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                  const SizedBox(width: 10),
                  Text(
                    title,
                    style: const TextStyle(
                        fontSize: 15, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
            Container(
              margin: const EdgeInsets.only(left: 15, top: 10),
              child: Row(
                children: [
                  Text(
                    value,
                    style: const TextStyle(
                        fontSize: 15, overflow: TextOverflow.ellipsis),
                  )
                ],
              ),
            ),
          ],
        ));
  }
}


```


```dart:test_screen.dart
import 'dart:math';
import 'package:flutter/material.dart';

///
/// アカウント情報編集画面
///
class DialogWidget {
  Widget dialogSampleWidget(BuildContext context) {
    final deviceHeight = MediaQuery.of(context).size.height;
    const userInfoSidePadding = 10.0;

    return AlertDialog(
      insetPadding: const EdgeInsets.symmetric(vertical: 30, horizontal: 10),
      content: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          Container(
            margin: const EdgeInsets.symmetric(vertical: 15),
            child: const Text(
              "アカウント情報編集",
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
          ),
          Column(
            children: [
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: userInfoSidePadding),
                child: _inputForm(context, "アカウント", "sample_account@test.com"),
              ),
              const Divider(
                color: Colors.grey,
                height: 10,
              ),
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: userInfoSidePadding),
                child: _inputForm(context, "名前", "サンプル　太郎"),
              ),
              const Divider(
                color: Colors.grey,
                height: 10,
              ),
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: userInfoSidePadding),
                child: _inputForm(context, "職業", "なし"),
              ),
            ],
          ),
          SizedBox(height: deviceHeight / 20),
          Row(
            children: [
              Expanded(
                flex: 4,
                child: Container(
                  width: double.infinity,
                  margin: const EdgeInsets.only(left: 20, right: 5, bottom: 30),
                  padding: const EdgeInsets.only(top: 5, bottom: 5),
                  child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor: Colors.white38,
                      ),
                      onPressed: () {
                        Navigator.pop(context);
                      },
                      child: Container(
                        margin: const EdgeInsets.only(top: 15, bottom: 15),
                        child: const Text("キャンセル"),
                      )),
                ),
              ),
              Expanded(
                flex: 5,
                child: Container(
                  width: double.infinity,
                  margin: const EdgeInsets.only(left: 5, right: 20, bottom: 30),
                  padding: const EdgeInsets.only(top: 5, bottom: 5),
                  child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor: Colors.green,
                      ),
                      onPressed: () {},
                      child: Container(
                        margin: const EdgeInsets.only(top: 15, bottom: 15),
                        child: const Text("変更する"),
                      )),
                ),
              )
            ],
          )
        ],
      ),
    );
  }

  Widget _inputForm(BuildContext context, String title, String hintText) {
    final MediaQueryData data = MediaQuery.of(context);

    return Container(
      margin: const EdgeInsets.only(top: 10, bottom: 10),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                height: 25,
                width: 5,
                decoration: BoxDecoration(
                  color: Colors.green,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
              const SizedBox(width: 10),
              Text(
                title,
                style:
                    const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          Container(
            margin: const EdgeInsets.all(10),
            child: MediaQuery(
              data: data.copyWith(
                  textScaleFactor: min(1.5, data.textScaleFactor)),
              child: TextFormField(
                  onChanged: (value) {},
                  autovalidateMode: AutovalidateMode.onUserInteraction,
                  decoration: InputDecoration(
                      fillColor: Colors.white70,
                      filled: true,
                      focusedBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8),
                        borderSide: const BorderSide(
                          color: Colors.white70,
                        ),
                      ),
                      contentPadding: const EdgeInsets.symmetric(
                        vertical: 10,
                        horizontal: 5,
                      ),
                      enabledBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8),
                        borderSide: const BorderSide(
                          color: Colors.white70,
                        ),
                      ),
                      hintText: hintText),
                  keyboardType: TextInputType.text),
            ),
          ),
        ],
      ),
    );
  }
}

```

## おわりに
現在、業務でもプライベートでも様々なUIをFlutterで開発しておりますが、
かなりスムーズに開発できて良いですね！！

Flutter自体クロスプラットフォーム開発できるフレームワークで
Android・iOSの開発が１つのソースコードでできるのでスムーズですが
そちらに加えてUI開発のしやすさも強みのひとつだと思っております。（Hot Reloadもありますし）

今回のようにたまにエラーでハマることもありますが、
これからもFlutterについて学習を進めていこうと思います(^^)