【Flutter】バリデーション付きテキストフォームを作る

# 前書き
bravesoft株式会社でエンジニアをしているfukuyamaです。
弊社ではアプリ開発においてFlutterでの開発業務も行なっており、Flutterのアプリ作成会(研修)も開催されています。
その中でEmailやPasswordのバリデーション、その他の技術についてもっと調べたいと思い、記事にしました。
今回は誰でも取り組めるFlutterのバリデーションを利用したテキストフォームの作成を行なってみます。

# 想定読者
- Flutterは触ったことがないが、プログラムを書いたことがある
実装難易度は ★☆☆☆☆ 程度ですので、年末に家族でみかんを片手に眺めてもらえればと思っています。
![toshikoshi_soba_animal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935045/78e08daf-2f47-ae3b-59db-1f57961744cd.png)


## バリデーションとは
入力されたデータが「不正な書き方じゃないか」「必要な文字の種類を使っているか」などを調べてエラー表示などをすること
![ダウンロード (5).jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935045/42066689-6140-0ba9-09a6-c14700e8a7df.jpeg)

## バリデーションを用いたformの作り方
今回は「7文字以上」の文字がある場合に限ってOKであるようなテキストフォームを作成します。
### 1. GlobalKeyでformを作る
まず、`GlobalKey`を使ってフォームを一意に識別できるようにします。
以下のように定義することで、アプリ内で唯一のWidgetとして定義することができます。
`final _formKey = GlobalKey<FormState>();`

:::note
`GlobalKey`を作成することで一種のStatefullWidget(状態を持つWidget)としてFlutterが認識してくれるため、フォームの状態を検知し、バリデーションができる様になります。
状態が変更されるたび、`validator:`に定義した関数を呼び出すことになり検証ができる認識です。
:::


### 2. バリデーションの処理を持たせたTextFormField Widgetを用意する
今回はFlutterにあるTextFormField Widgetを使用します。
`validator:` に検証するための関数を渡します。 
```sample.dart
TextFormField(
  validator: viewModel.validateTextField,
)
```
今回は、`validator: viewModel.validateTextField` の様に別のクラスで定義してある関数を渡します
```main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'main_view_model.dart';

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

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

  @override
  Widget build(BuildContext context) {
    const appTitle = 'サンプルアプリ';

    return MaterialApp(
      title: appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(appTitle),
        ),
        body: ChangeNotifierProvider(
          create: (context) => MainViewModel(),
          child: const MyCustomForm(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<MainViewModel>(context);

    return Padding(
      padding: const EdgeInsets.all(20),
      child: Form(
        key: viewModel.formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // ここです
            TextFormField(
              validator: viewModel.validateTextField,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () => viewModel.submitForm(context),
                child: const Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

### 3. formの内容をバリデート(検証)するための関数を設置する
MainViewModel　という名前のクラスを作成します。そこにSubmitボタンと、バリデーションの関数を定義しましょう。
（MVVM設計の「VM」=ViewModelに当たる部分として実装しました）
```main_view_model.dart
import 'package:flutter/material.dart';

class MainViewModel extends ChangeNotifier {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  
  String? validateTextField(String? value) {
    if (value == null || value.isEmpty) {
      return '何か文字を入れてください';
    }
    if (value.length < 7) {
      return '7文字以上で入力してください';
    }
    return null;
  }

  void submitForm(BuildContext context) {
    final currentState = formKey.currentState;
    if (currentState?.validate() ?? false) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('成功しました！'),
          backgroundColor: Colors.green,
        ),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('失敗しました。入力を確認してください。'),
          backgroundColor: Colors.red,
        ),
      );
    }
  }
}
```

`submitForm`関数内では、定義したformの状態を確認し、バリデーション関数の検証を通過したものに限って処理を行います。
以下で、if文の中の各要素の意味を簡単に説明します。

- `formKey`: GlobalKeyとして定義したWidgetを指す 
- `currentState`: そのWidjetの「監視されている状態」を指す
- `validate()`: `validator:`に渡された関数の実行を指す。フォーム内の全てのバリデーションが通過した場合に限りtrueを返します。

## サンプル(完成物)
空文字や7字以下はNGなフォームが完成しました。同じようにしてパスワードやEmailのバリデーションも作成できる様になります
- 1文字ではNG
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935045/626c2c21-5b9f-f07d-f286-68b4e42f1f79.png">

- 空文字もNG
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935045/ae7876ad-f3f9-bd4d-a92b-57c3f80c755d.png">

- 7文字以上はOK
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3935045/e0e78dd2-b0cc-e4b8-9521-e4a2328112b4.png">




# MORE INFO
### グローバルキーとローカルキー
グローバルキーはWidget全体を一意に指したいときに使います
- フォーム・ナビゲーション　など
:::note
上記でも述べましたが、今回は`GlobalKey`を作成することでFormクラスを一種のStatefullWidget(状態を持つWidget)としてFlutterが認識してくれるため、フォームの状態を検知し、バリデーションができる様になります。
Stateless/fulの違いについては最後にサクッと説明します
:::

ローカルキーはWidgetの一部を一意に指したいときに使います
- ListView　など
```sample.dart
ListView(
  children: <Widget>[
    Container(
      key: LocalKey('item1'),
      // コンテナをユニークにしたいときに使う
    ),
    Container(
      key: LocalKey('item2'),
      // 上のコンテナとは別物としたい。。、時など
    ),
  ],
);
```
### メールアドレスのバリデーション
以下の様にメールアドレス/パスワードの形式も、正規表現を用いて検証させることができます
(RegExp などについては調べてみてください)
```sample.dart
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
  return "メールアドレスの形式が不正です";
}
```

### プロバイダーについて（状態管理）
最近はSwiftUIで状態をラップしていることもあり、Flutterの感覚を取り戻すためにも書いておきます。
#### ChangeNotifier
- 状態を管理し、変更を伝えるためのクラスです。これのクラスを継承させることで、データの変更をViewなどの他のクラスに伝えることができます
:::note
ViewModelなど値の変化を扱うクラスに継承させることが多いと思います
:::

```sample.dart
class MainViewModel extends ChangeNotifier
```


#### ChangeNotifierProvider
ChangeNotifierProviderは、ChangeNotifierのインスタンス状態変更をウィジェットツリーに提供するためのProviderクラスです。
**これを書くことで画面表示としての変更が行われることになります**
- create: ChangeNotifierつまり監視対象のViewModelを指定します
- child:　お好みのWidgetを配置してOKです
→「childに指定したWidget配下から、createで指定したViewModelにアクセスできる様にする」という感じです。

```sample.dart
body: ChangeNotifierProvider(
    create: (context) => MainViewModel(),
    child: const MyCustomForm(),
),
```

#### viewmodelインスタンス
```sample.dart
class MyCustomForm extends StatelessWidget {
  const MyCustomForm({super.key});

  @override
  Widget build(BuildContext context) {
    final viewModel = Provider.of<MainViewModel>(context);
    ...
```
としてあげることで、ViewでViewModelのメソッドやプロパティにアクセスできます

#### Stateless/Stateful Widgetの違いについてサクッと説明
◎状態管理
- StatelessWidgetは状態を持たない。
- StatefulWidgetは状態を持ち、状態が変更されるとUIが再構築される。

◎使用シーン
- StatelessWidgetは、状態が一度設定されたら変更されないUIを作成する時に使う。
- StatefulWidgetは、ユーザーの入力やデータの変更に応じて動的にUIを更新する必要がある場合に使う。

# 最後に
最初はStatefulWidgetを使って記事を書いていましたが、プロバイダーを使いたくなったので、StatelessなWidgetとして実装し、軽くProviderの紹介もしてみました。

小さく新しい技術へのキャッチアップを始めてみてはいかがでしょうか。
弊社のFlutterエンジニアは多いとは言えないので、素敵な人材がもっと集まればいいなあと思っています。

ご一読ありがとうございました、良いお年を。メリクリ。