🐹

画面とロジックを分ける

2023/06/19に公開

Riverpodではモデルクラス以外も使えるのあるんだよ!

最近、僕のコードの書き方が、UIとロジックを分けるのが中途半端でかっこよくないと思い、Riverpod2.0で非推奨になったコードを使わずに、推奨されているNotifierを使って、エラーメッセージをUIに表示するだけですが、やってみました。

フォルダ構成

doc_notifierというフォルダは、実験用に作ったコードが配置されています。使っているのは、エラーメッセージを保持するクラスがあるmodelフォルダ、UIを表示するscreenフォルダ、画面に渡す状態が書かれているstoreフォルダ、他の機能と連携してないファイルを配置しているcommonフォルダに分けてファイル管理をしています。

lib/
├── common
│   └── form_provider.dart
├── doc_notifier
│   ├── notifier_class.dart
│   └── state_class.dart
├── main.dart
├── model
│   └── error_msg.dart
├── screen
│   └── form_example_notifier.dart
└── store
    └── form_validator.dart

状態を保持するクラス

これだけしか書いてないですが、これがエラーメッセージを表示するのに必要なクラスです。Riverpod使うときは、Freezedでモデルクラスを使ってただけなんですけど、知り合いの人でアーキテクチャの設計にこだわっている人を見て、状態を保持するクラスを使っているのを見て、画面に状態を渡す方法を今回学習してみました。

model/error_msg.dart
// ErrorMsgStateは、名前とメールアドレスのエラーメッセージを保持するクラスです。
class ErrorMsgState {
  final String? nameError;
  final String? emailError;

  ErrorMsgState({this.nameError, this.emailError});
}

状態を管理するクラス

Riverpod2.0から非推奨になったStateProviderとStateNotifierの代わりに使われるようになったNotifierを使用して状態管理をします。入力に使うTextEditingControllerもこちらを使用します。

store/form_validator.dart
import 'package:flutter_new/model/error_msg.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// FormValidatorNotifierは、ErrorMsgStateを使用して、名前とメールアドレスのエラーメッセージを保持します。
class FormValidatorNotifier extends Notifier<ErrorMsgState> {
  
  build() {
    return ErrorMsgState();
  }

  void validate({String? name, String? email}) {
    String? nameError;
    String? emailError;

    if (name == null || name.isEmpty) {
      nameError = "名前が入力されていません";
    }
    if (email == null || email.isEmpty) {
      emailError = "メールアドレスが入力されていません";
    }

    state = ErrorMsgState(nameError: nameError, emailError: emailError);
  }
}

final formValidatorNotifierProvider =
    NotifierProvider<FormValidatorNotifier, ErrorMsgState>(
        FormValidatorNotifier.new);

TextEditingControllerはこちらのcommonフォルダに、配置して状態管理をしています。今後は、このような書き方が推奨されるみたいですね。

common/form_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// NameNotifierは、名前を保持します。
class NameNotifier extends Notifier<TextEditingController> {
  
   build() {
    return TextEditingController(text: 'NameNotifier');
  }
}

// EmailNotifierは、メールアドレスを保持します。
class EmailNotifier extends Notifier<TextEditingController> {
  
   build() {
    return TextEditingController(text: 'EmailNotifier');
  }
}

// nameNotifierProviderは、NameNotifierを使用して、名前を保持します。
final nameNotifierProvider = NotifierProvider<NameNotifier, TextEditingController>(NameNotifier.new);
// emailNotifierProviderは、EmailNotifierを使用して、メールアドレスを保持します。
final emailNotifierProvider = NotifierProvider<EmailNotifier, TextEditingController>(EmailNotifier.new);

UIでエラーメッセージを表示する

こちらは、状態を渡して画面にスナックバーを表示するファイルです。

screen/form_example_notifier.dart
// ConsumerWidget
// ignore: must_be_immutable
import 'package:flutter/material.dart';
import 'package:flutter_new/common/form_provider.dart';
import 'package:flutter_new/store/form_validator.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class FormExampleNotifier extends ConsumerWidget {
  final _formKey = GlobalKey<FormState>();

  FormExampleNotifier({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    // nameNotifierProviderとemailNotifierProviderを使用して、名前とメールアドレスを取得します。
    final name = ref.watch(nameNotifierProvider);
    final email = ref.watch(emailNotifierProvider);
    // formValidatorNotifierProviderを使用して、ErrorMsgStateを取得します。
    ref.listen(formValidatorNotifierProvider, (previous, next) {
      if (next.nameError != null) {
        // nameErrorがnullでない場合は、SnackBarを表示します。
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(next.nameError.toString()),
          ),
        );
      } else if (next.emailError != null) {
        // emailErrorがnullでない場合は、SnackBarを表示します。
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(next.emailError.toString()),
          ),
        );
      } else {
        // 入力が完了した場合は、SnackBarを表示します。
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Notifierで送信完了'),
          ),
        );
      }
    });
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurple,
        title: const Text('Form Example Notifier'),
      ),
      body: Form(
        key: _formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SizedBox(
              width: 300,
              child: TextFormField(
                controller: name,
                decoration: InputDecoration(labelText: '名前'),
              ),
            ),
            const SizedBox(height: 20),
            Container(),
            SizedBox(
              width: 300,
              child: TextFormField(
                controller: email,
                decoration: InputDecoration(labelText: 'メールアドレス'),
              ),
            ),
            ElevatedButton(
              onPressed: () {
                ref
                    .read(formValidatorNotifierProvider.notifier)
                    .validate(name: name.text, email: email.text);
              },
              child: Text('送信'),
            ),
          ],
        ),
      ),
    );
  }
}

アプリを実行してみる

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_new/screen/form_example_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: FormExampleNotifier(),
    );
  }
}

実行結果
ref.listenを使用して、スナックバーでメッセージを表示します。入力がされていないと、エラーを表示、送信に成功したら、送信に成功したメッセージを表示します。



まとめ

今回作ったサンプルですけど、MVVMと呼べるものではないです😅
新しいプロバイダーを学習するために作った学習用のサンプルです。完成品のコードもこちらに公開しておきます。
https://github.com/sakurakotubaki/StateClassErrorMsg

Discussion