【誰でも分かる!】FlutterのFormを一から作りながら解説してみたよ

Dart

概要


「メールアドレス、パスワードの入力フォーム作りたいけどどうやれば良いかわからない…」
「TextFieldとか使って作ってみたけどコードがぐちゃぐちゃ…」

そんな悩みを抱えていませんか?

アプリを作っていると、入力フォームってほぼ確実に一回は作りますよね。
そんな入力フォーム作成に特化したForm関連ウィジェットについて本記事で解説していきたいと思います。

バリデーションや保存時の処理など充実しているのでとても綺麗にコーディング出来ますよ!

ソースコードは以下に置いています。

GitHub - daiki1003/form_example: form widget example with Flutter.
form widget example with Flutter. Contribute to daiki1003/form_example development by creating an account on GitHub.

前提

Flutterプロジェクトを作成し、出来上がった_MyHomePageStateのbody部分を中心に記載していきます。

FlutterにおけるFormとTextFormField

まず、Formで包んだTextFormFieldを用意してみる

// main.dart
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        child: TextFormField(
          decoration: InputDecoration(labelText: 'Title'),
        ),
      ),
    );
  }

※以降はbodyのみ

実行結果

解説

これだけで、アニメーションなどが実装された
ちょっと良い感じのテキストフォームが出来上がるの本当すごい。

キーボードを二つ並べる

// main.dart
Form(
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'id'),
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
      ),
    ],
  ),
)

実行結果

解説

はい、2つ並びました。

ここで一つポイントです。

FormはTextFormFieldなどFormFieldの小クラスと裏で連携していますが、それしか置けないわけではない

と言うことです。
まぁ、当たり前ですよね笑
なので、今回の例のようにColumnなんかをchildとして指定することは出来ます。

入力中のパスワードを非表示にする

// main.dart
Form(
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'id'),
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
        obscureText: true, // 追加
      ),
    ],
  ),
)

実行結果

解説

obscureTextをtrueにすれば、入力したテキストが隠れる様になります。

doneボタンが押された時の処理を記述したい

// main.dart
Form(
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'id'),
        textInputAction: TextInputAction.next,
        onFieldSubmitted: (value) {
          print(value);
        }, // 追加
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
        obscureText: true,
      ),
    ],
  ),
)

実行結果

引数valueには現在入力されている値が渡されます。
よって、idに何か入力してキーボード右下のボタンを押すと
コンソールに現在入力されているidの値が表示されます。

キーボードの右下ボタンをdoneからnextに変更

// main.dart
Form(
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'id'),
        textInputAction: TextInputAction.next, // 追加
        onFieldSubmitted: (value) {
          print(value);
        },
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
        obscureText: true,
      ),
    ],
  ),
)

実行結果

解説

nextになりました。
が、 これだけではpasswordに移動してはくれません。

idのTextFormFieldは「doneをnextに変えろ」と言われただけで
nextを押した際の挙動を把握しているわけではないからです。

passwordに移動する

// main.dart
@override
Widget build(BuildContext context) {
  final _passwordFocusNode = FocusNode(); // 追加

  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Form(
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: 'id'),
            textInputAction: TextInputAction.next,
            onFieldSubmitted: (_) {
              FocusScope.of(context).requestFocus(_passwordFocusNode); // 変更
            }, 
          ),
          TextFormField(
            focusNode: _passwordFocusNode, // 追加
            decoration: InputDecoration(labelText: 'password'),
            obscureText: true,
          ),
        ],
      ),
    ),
  );
}

実行結果

解説

では、nextを押した際の挙動を教えてあげましょう。
と言うことで、これで無事にpasswordに移動しましたね。

FocusNode インスタンスを生成
・移動したいTextFormFieldにFocusNodeインスタンスを割り当てる
・FocusScope.of(context).requestFocus()で対象のTextFormFieldにフォーカスを当てる

と言う流れで実行できます。

初期値を設定する

// main.dart
Form(
  child: Column(
    children: [
      TextFormField(
        initialValue: 'initial id', // 追加
        decoration: InputDecoration(labelText: 'id'),
        textInputAction: TextInputAction.next,
        onFieldSubmitted: (_) {
          FocusScope.of(context).requestFocus(_passwordFocusNode);
        },
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
        obscureText: true,
        focusNode: _passwordFocusNode,
      ),
    ],
  ),
)

実行結果

現状の入力値を保存したい

// main.dart
@override
Widget build(BuildContext context) {
  final _passwordFocusNode = FocusNode();
  final _form = GlobalKey<FormState>(); // 追加
  String _userId;
  String _password;

  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Form(
      key: _form, // 追加
      child: Column(
        children: <Widget>[
          TextFormField(
            decoration: InputDecoration(labelText: 'id'),
            textInputAction: TextInputAction.next,
            onFieldSubmitted: (_) {
              FocusScope.of(context).requestFocus(_passwordFocusNode);
            },
            onSaved: (value) {
              _userId = value;
            }, // 追加
          ),
          TextFormField(
            decoration: InputDecoration(labelText: 'password'),
            obscureText: true,
            focusNode: _passwordFocusNode,
            onSaved: (value) {
              _password = value;
            }, // 追加
          ),
          FlatButton(
            child: Text('Save'),
            color: Colors.grey,
            onPressed: () {
              _form.currentState.save();
              print(_userId);
              print(_password);
            },
          ), // 追加
        ],
      ),
    ),
  );
}

実行結果

flutter: test
flutter: testpassword

解説

まず、GlobalKey<FormState>インスタンスを生成します。(5行目)
このGlobalKeyインスタンスが持つcurrentStateで指定したState(今回はFormState)のインスタンスが取得できます。
このFormStateインスタンスが持つsaveメソッドを呼び出すことによってFormの内容を保存する事が出来ます。

では、Formの保存とは一体何をしているのでしょうか?

保存をすると、各TextFormFieldのonSavedメソッドが呼ばれます。(23行目、31行目)
例では、onSavedメソッドで、自信が持つプライベート変数に代入するようにしています。

saveメソッドを実行し終えた後(39行目実行後)は各widgetのonSavedが動いた後なので、
無事にprintが動作すると言う流れです。

バリデーションしたい

// main.dart
Form(
  key: _form,
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'id'),
        textInputAction: TextInputAction.next,
        validator: (value) {
          if (value.isEmpty) {
            return 'Please provide a value.';
          }
          if (value.length <= 4) {
            return 'id must be longer than 4 characters.';
          }
          if (16 < value.length) {
            return 'id must be less than 16 characters.';
          }
          return null;
        }, // 追加
        onFieldSubmitted: (_) {
          FocusScope.of(context).requestFocus(_passwordFocusNode);
        },
        onSaved: (value) {
          _userId = value;
        },
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'password'),
        obscureText: true,
        focusNode: _passwordFocusNode,
        validator: (value) {
          if (value.isEmpty) {
            return 'Please enter a password.';
          }
          return null;
        }, // 追加
        onSaved: (value) {
          _password = value;
        },
      ),
      FlatButton(
        child: Text('Save'),
        color: Colors.grey,
        onPressed: () {
          _form.currentState.save();
        },
      ),
    ],
  ),
)

実行結果

解説

見てお分かりのとおり、validatorに
現在の入力値を引数にとり、エラー文言を返却するFunction
を設定します。

エラーがなければnullを返す様にします。

その他バリデーションに使えるメソッド達

数値かどうか確かめたい

// 整数
if (int.tryParse(value) == null) {
  return 'Input a valid number.';
}

// 小数値
if (double.tryParse(value) == null) {
  return 'Input a valid number.';
}

指定した文字列で始まる|終わるか確かめたい

if (!value.startsWith('http')) {
  return 'Input a valid url.';
}

if (!value.endsWith('.png')) {
  return 'Input a png format url';
}

この他にも多種多様なプロパティが指定出来るので試してみてください。

最後に

ここまで読んでいただいてありがとうございます!
明日からはとうとうFirebaseとの連携について解説したいと思います!
お楽しみに!

誰かのお役に立てば。

Twitterフォローお願いします

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非Twitter (@daiki1003)Instagram (@ashdik_flutter)のフォローお願いします♪

Twitterコミュニティ参加お願いします

Twitterコミュニティ「Flutter lovers」を開設しました!
参加お待ちしております😁

☕️ Buy me a coffee

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!

コメント

  1. […] TextFormField  → 【誰でも分かる!】FlutterのFormを一から作りながら解説してみたよ 2020.6.3 […]

タイトルとURLをコピーしました