6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Flutter] formz+riverpodを使ったフォームの実装例

Posted at

はじめに

formz はFlutterにおけるフォーム入力+バリデーションの管理を簡略化する為のライブラリです。

この記事では、formzとriverpodを使って簡単なフォームを実装してみます。
まずはフォーム用の画面を用意します。

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(32.0),
          child: Center(
            child: Column(
              children: <Widget>[
                TextField(
                  decoration: const InputDecoration(labelText: 'ユーザー名'),
                  keyboardType: TextInputType.emailAddress,
                  onChanged: (value) {
                  },
                ),
                TextField(
                  decoration: const InputDecoration(labelText: 'パスワード'),
                  onChanged: (value) {
                  },
                ),
                const SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                  onPressed: () {
                  },
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

FormzInputの作成

formzを使ったフォームの実装を行っていきます。

formzではFormzInputというクラスを継承してフォームを管理するモデルを作成します。
まずはREADMEに従って、FormzInputを用意します。

// ユーザー名入力用
class UserNameInput extends FormzInput<String, UserNameInputError> {
  const UserNameInput.pure() : super.pure('');
  const UserNameInput.dirty({String value = ''}) : super.dirty(value);

  @override
  UserNameInputError? validator(String value) {
    return value.isEmpty ? UserNameInputError.empty : null;
  }
}

// ユーザー名入力エラー用
enum UserNameInputError {
  empty(errorText: '未入力です');

  const UserNameInputError({required this.errorText});

  final String errorText;
}


// パスワード入力用
class PasswordInput extends FormzInput<String, PasswordInputError> {
  const PasswordInput.pure() : super.pure('');
  const PasswordInput.dirty({String value = ''}) : super.dirty(value);

  @override
  PasswordInputError? validator(String value) {
    if (value.length < 8) return PasswordInputError.tooShort;
    if (!value.contains(RegExp(r'[0-9]'))) {
      return PasswordInputError.noDigits;
    }
    if (!value.contains(RegExp(r'[A-Z]'))) {
      return PasswordInputError.noUppercase;
    }
    if (!value.contains(RegExp(r'[a-z]'))) {
      return PasswordInputError.noLowercase;
    }
    return null;
  }
}

// パスワード入力エラー用
enum PasswordInputError {
  tooShort(errorText: '8文字以上入力して下さい'),
  noDigits(errorText: "数字が含まれていません"),
  noUppercase(errorText: "アルファベットの大文字が含まれていません"),
  noLowercase(errorText: "アルファベットの小文字が含まれていません");

  const PasswordInputError({required this.errorText});

  final String errorText;
}

FormzInput継承時の型指定で、そのフォームの入力値の型とエラーの型が決まります。

FormzInput<String, NameInputError>

一番目の型引数がフォーム入力値の型です。二番目がエラーの型で、これをバリデーションのエラー時に返却します。

コンストラクタには puredirty という2つのfactoryコンストラクタを用意します。
pure はフォームが未入力状態を表すコンストラクタです。 super.pure('') と親クラスのコンストラクタに値を渡すことでフォームの初期値を設定します。
dirty はフォームの入力後を表すコンストラクタになります。

バリデーションのロジックは validator メソッドをoverrideして記述します。
このメソッドの返却値の型は、継承時に指定したエラーの型をNull可にしたものになります。入力値が正常の場合にはnullを返却します。

RiverpodのNotifierProviderの作成

RiverpodのNotiferProviderを使ってフォームの状態管理を行います。

※Riverpodはv2のコード生成を利用しています。またフォーム全体をまとめるモデルクラスはfreezedを使っています

@freezed
class SampleForm with _$SampleForm {
  factory SampleForm({
    required UserNameInput userName,
    required PasswordInput password,
    required bool isValid,
  }) = _SampleForm;
}

@riverpod
class SampleFormController extends _$SampleFormController {
  @override
  SampleForm build() {
    return SampleForm(
        userName: const UserNameInput.pure(),
        password: const PasswordInput.pure(),
        isValid: false);
  }

  void onChangeUserName(String value) {
    final userName = UserNameInput.dirty(value: value);
    state = state.copyWith(
        userName: userName,
        isValid: Formz.validate([
          userName,
          state.password,
        ]));
  }

  void onChangePassword(String value) {
    final password = PasswordInput.dirty(value: value);
    state = state.copyWith(
        password: password,
        isValid: Formz.validate([
          password,
          state.userName,
        ]));
  }

  Future<void> submit() async {
    if (!state.isValid) return;

    // submit処理...
  }

NotiferProviderで、stateの初期状態を定義する build メソッドでは、先程用意したFormzInputの pure
を使って、初期状態を未入力として返却しています。

return SampleForm(
  userName: const UserNameInput.pure(),
  password: const PasswordInput.pure(),
  isValid: false);

また、入力を受け付けるメソッド、 onChangeUserNameonChangePassword では、 dirty で入力済みのFormzInputを生成し、その後バリデーションを実行して結果を isValid に代入しています。

final userName = UserNameInput.dirty(value: value);
state = state.copyWith(
  userName: userName,
  isValid: Formz.validate([
      userName,
      state.password,
]));

UIにバリデーションの結果を反映する

最後にフォームから入力を監視して、バリデーションの結果をUIに反映したいと思います。

MainAppは以下のようになりました。

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sampleForm = ref.watch(sampleFormControllerProvider);

    return MaterialApp(
      home: Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(32.0),
          child: Center(
            child: Column(
              children: <Widget>[
                TextField(
                  decoration: const InputDecoration(labelText: 'ユーザー名'),
                  keyboardType: TextInputType.emailAddress,
                  onChanged: ref
                      .read(sampleFormControllerProvider.notifier)
                      .onChangeUserName,
                ),
                if (sampleForm.userName.displayError != null)
                  Text(sampleForm.userName.displayError!.errorText,
                      style: TextStyle(
                        color: Theme.of(context).colorScheme.error,
                      )),
                TextField(
                  decoration: const InputDecoration(labelText: 'パスワード'),
                  onChanged: ref
                      .read(sampleFormControllerProvider.notifier)
                      .onChangePassword,
                ),
                if (sampleForm.password.displayError != null)
                  Text(sampleForm.password.displayError?.errorText ?? '',
                      style: TextStyle(
                        color: Theme.of(context).colorScheme.error,
                      )),
                const SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                  onPressed: sampleForm.isValid
                      ? ref.read(sampleFormControllerProvider.notifier).submit
                      : null,
                  child: const Text('送信'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
  1. riverpodで入力を監視できるよう、StatelessWidget -> ConsumerWidget に変更しました。
  2. バリデーションエラーがあった場合だけエラーテキストを表示するようにしています。
    if (sampleForm.userName.displayError != null)
      Text(sampleForm.userName.displayError?.errorText ?? '',
        style: TextStyle(
          color: Theme.of(context).colorScheme.error,
      )),
    

※displayErrorは dirtyの場合だけエラーを返却するので初期状態でエラーにならない
3. ボタンの活性/非活性状態を、 isValid で判定するようにしています。

おわり

formz+riverpodを使った簡単なフォーム実装についてまとめてみました!

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?