Flutter フォーム入力は、TextFormFieldとFormを組み合わせると実装しやすくなります。さらにvalidatorで入力内容を確認し、FormState.validate()がtrueのときだけ送信処理を実行するのが基本です。
この記事では、入力欄を1つ表示する最小構成から、未入力エラーの表示、ボタンを押したときの判定、初心者がつまずきやすい点までを順番に解説します。
初心者の疑問: TextFieldとTextFormFieldは、どちらを使えばよいですか?
入力チェックを行うフォームなら、まずTextFormFieldを選ぶのがおすすめです。TextFormFieldはTextFieldをFormFieldとして扱えるため、Formのバリデーションと組み合わせられます。単純な検索欄など、個別のチェックが不要な場合はTextFieldでも問題ありません。
Flutterのフォーム入力で使う部品
最初に、各部品の役割を整理しましょう。
| 部品 | 役割 | 初心者が覚えるポイント |
|---|---|---|
Form | 複数の入力欄をまとめる | フォーム全体を検証できる |
GlobalKey<FormState> | Formの状態を取得する | validate()を呼ぶために使う |
TextFormField | 文字入力欄を表示する | validatorで入力チェックできる |
validator | 入力が正しいか判定する | エラー時は文字列、問題なければnullを返す |
ElevatedButton | 送信ボタンを表示する | validate()の結果を見て処理する |
ポイント: フォーム入力は「表示」「検証」「送信」を分けて考えると、コードの役割が分かりやすくなります。
STEP1:フォーム用のStatefulWidgetを作る
GlobalKey<FormState>は、フォームの状態を参照するためのキーです。buildの中で毎回作り直すと状態管理が不安定になるため、Stateクラスのフィールドとして1回だけ作ります。
import 'package:flutter/material.dart';
class ContactForm extends StatefulWidget {
const ContactForm({super.key});
@override
State<ContactForm> createState() => _ContactFormState();
}
class _ContactFormState extends State<ContactForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: const Column(
children: [],
),
);
}
}
Formは複数の入力欄をまとめるコンテナです。入力欄が1つでも、後から項目を増やす予定があるなら、この形で始めておくと拡張しやすくなります。
初心者の疑問: なぜStatelessWidgetではなくStatefulWidgetにするのですか?
フォームの状態を保持するGlobalKeyを、画面のライフサイクルに合わせて管理しやすくするためです。特に、buildのたびに新しいキーを作らないことが重要です。
STEP2:TextFormFieldに入力チェックを追加する
次に、Formの中へTextFormFieldを追加します。validatorは入力値を受け取り、問題があるときにエラーメッセージを返します。問題がなければnullを返してください。
TextFormField(
decoration: const InputDecoration(
labelText: '名前',
hintText: '名前を入力してください',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '名前を入力してください';
}
return null;
},
),
trim()を使うと、半角スペースだけを入力した場合も空欄として扱えます。メールアドレスや文字数などを検証したい場合も、同じvalidatorの中に条件を追加できます。
validator: (value) {
final email = value?.trim() ?? '';
if (email.isEmpty) {
return 'メールアドレスを入力してください';
}
if (!email.contains('@')) {
return 'メールアドレスの形式を確認してください';
}
return null;
},
注意: validatorで「問題ありません」と表示する必要はありません。エラーがないときはnullを返すのがFlutterのルールです。
STEP3:ボタンで検証してから送信する
入力欄と送信ボタンをFormの子要素に置き、ボタンのonPressedでフォームを検証します。
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '名前',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '名前を入力してください';
}
return null;
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('入力を受け付けました')),
);
}
},
child: const Text('送信'),
),
],
),
)
validate()は、Formの中にある各フィールドのvalidatorを実行します。すべて問題なければtrue、1つでもエラーがあればfalseです。実際のアプリでは、trueのブロック内でAPI送信やデータ保存を行います。
初心者の疑問: 入力が空なのに送信処理が実行されるのはなぜですか?
TextFormFieldがFormの子になっていない、ボタンで別のキーを使っている、またはvalidate()の結果を確認していない可能性があります。まず3つの部品が同じFormと_formKeyにつながっているか確認してください。
完成した最小サンプル
画面として動かす場合は、次のコードをScaffoldのbodyなどに配置できます。
class ContactForm extends StatefulWidget {
const ContactForm({super.key});
@override
State<ContactForm> createState() => _ContactFormState();
}
class _ContactFormState extends State<ContactForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '名前',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '名前を入力してください';
}
return null;
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('送信しました')),
);
}
},
child: const Text('送信'),
),
],
),
),
);
}
}
チェックリスト: Formにキーを設定した、入力欄がTextFormFieldになっている、validatorがある、エラー時に文字列を返す、validate()がtrueのときだけ送信している、の5点を確認しましょう。
よくある失敗と改善方法
失敗1:キーをbuildの中で作る
// 避けたい例
final formKey = GlobalKey<FormState>();
これをbuild内に置くと、再ビルドのたびにキーが変わります。キーはStateクラスのフィールドに置きます。
失敗例: buildの中で毎回GlobalKeyを生成し、入力欄の状態や検証対象が期待どおりにならない。
改善例: final _formKey = GlobalKey<FormState>();をStateクラスに1つだけ定義する。
失敗2:見た目だけのTextFieldを使う
TextFieldでも文字は入力できますが、Formのvalidate()で一括検証する用途にはTextFormFieldが向いています。入力チェックが必要かどうかで使い分けましょう。
失敗3:検証前にAPIへ送信する
送信ボタンを押したらすぐAPIを呼ぶのではなく、最初にvalidate()を実行します。さらに、送信中はボタンを無効にする、通信失敗時の表示を追加する、といった実装も本番では必要です。
補足: validatorは画面上の入力形式を確認する処理です。メールアドレスの存在確認やログイン認証の成否は、サーバー側の処理も必要です。
Flutterフォーム入力を実装したあとの次の作業
まずは名前の1項目で動作を確認し、その後にメールアドレスやパスワードを追加しましょう。複数項目にする場合も、各TextFormFieldへvalidatorを設定し、最後に1回のvalidate()でまとめて判定できます。
次にやること: 名前の入力チェックを動かしたら、メールアドレスの形式チェックを追加し、空欄・スペースだけ・正しい入力の3パターンをテストしてください。
まとめ
Flutter フォーム入力の基本は、Formで入力欄をまとめ、TextFormFieldのvalidatorでエラーを返し、GlobalKey<FormState>からvalidate()を呼ぶことです。
StateクラスにGlobalKey<FormState>を作るFormにキーを設定するTextFormFieldとvalidatorを追加する- ボタンで
validate()を呼び、trueのときだけ送信する
この順番で最小のフォームを作ってから、複数項目、文字数チェック、通信処理へ広げると、原因を切り分けながら実装できます。

コメント