【Flutter】RiverpodでTextEditingControllerを使いたい

# はじめに

久しぶりの投稿となります。うらさくです。

この記事は[TechCommit AdventCalendar2022](https://adventar.org/calendars/7673)の7日目の記事です。

昨日はにしやまさんの[Goでdiffコマンド書いて実際の実装と比べてみた](https://qiita.com/nsym__m/items/7bbdb162f513022bc461)でした

# 概要

Flutterアプリ開発中にいくつかメモしておきたいTipsがあったので、こちらを今回のアドカレのネタにしようと思います。

今回の題材はTextEditingControllerです。

TextEditingControllerは、Flutterのテキスト入力ウィジェット（例えばTextFieldやTextFormField）で使用される、テキスト入力を管理するためのクラスです。

これをRiverpod＋freezedの構成で使用する際に、**どこでTextEditingControllerを管理するか**で少し迷ったので、備忘録として記事に収めたいと思います

# 構成

下記のようなテキストボックスとテキストボタンのシンプルな構成で考えていきます

```dart:sample_page.dart
class SamplePage extends ConsumerWidget {
  const SampleView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sample = ref.watch(sampleNotifierProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        title: const Text("サンプルアプリ"),
      ),
      body: Column(
        children: [
          TextField(
            maxLines: 3,
            minLines: 1,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(5),
              ),
            ),
          ),
          TextButton(
            onPressed: () {sample.submit()},
            child: const Text('登録'),
          ),
        ],
      ),
    );
  }
}
```

テキストの状態をfreezedクラスとして保持しており、それをStateNotifierProviderで管理しています

```dart:sample_page_state.dart
@freezed
class SamplePageState with _$SamplePageState {
  const factory SamplePageState({
    @Default("") String text,
  }) = _SamplePageState;
}
```

Notifierには登録処理を持たせており、ボタン押下時にstateのtextの登録処理を行います

```dart:sample_page_notifier.dart
final SamplePageStateProvider =
  StateNotifierProvider.autoDispose<SamplePageNotifier, SamplePageState>((ref) => SamplePageNotifier(ref)
);

class SamplePageNotifier extends StateNotifier<SamplePageState> {
  final Ref ref;

  SamplePageNotifier(this.ref) : super(SamplePageState());

  // 登録する
  void submit() {
    // 登録処理
  }
}
```
このTextFieldで使うTextEditingControllerをどこで管理すれば良いのかを考えていきます

# ①Viewで管理する

StatefulWidgetと同じように、ViewでTextEditingControllerを管理することも可能です

```diff_dart:sample_page.dart
class SampleView extends ConsumerWidget {
  const SampleView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sample = ref.watch(sampleNotifierProvider.notifier);

+   TextEditingController _controller = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: const Text("サンプルアプリ"),
      ),
      body: Column(
        children: [
          TextField(
+           controller: _controller,
            maxLines: 3,
            minLines: 1,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(5),
              ),
            ),
          ),
          以下略
```

ただViewで管理してしまうと、stateのtextと実際の入力値が即座にリンクされないため、登録処理の際にviewからtextをnotifierに渡してあげる必要があります

```diff_dart:sample_page.dart
        TextButton(
-           onPressed: () {sample.submit()},
+           onPressed: () {sample.submit(_controller.text)},
            child: const Text('登録'),
          ),
```

```diff_dart:sample_page_notifier.dart
class SamplePageNotifier extends StateNotifier<SamplePageState> {
  final Ref ref;

  SamplePageNotifier(this.ref) : super(SamplePageState());

  // 登録する
-  void submit() {
-    // 登録処理
-  }

+  void submit(String text) {
+    // 登録処理
+  }
}
```

こうなってくるともはやstateを保持する意味がほとんどなくなるケースもあるかと思います

まだ項目が一つなので問題なさそうに見えますが、フォームが増えてくると、その分全てを送らないといけないのでかなり面倒なことになります

なので、**stateとTextEditingControllerのtextを同期させたくないケース**くらいしか使わないかなと思います（そんなケースはあまり思い浮かばないので、つまりほとんど使わないと思います）

# ②freezedで管理する

freezedクラスにTextEditingControllerを埋め込み、stateとして保持する方法です

```diff_dart:sample_page_state.dart
@freezed
class SamplePageState with _$SamplePageState {
  const factory SamplePageState({
-    @Default("") String text,
+    TextEditingController text,
  }) = _SamplePageState;
}
```
```diff_dart:sample_page.dart
class SampleView extends ConsumerWidget {
  const SampleView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sample = ref.watch(sampleNotifierProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        title: const Text("サンプルアプリ"),
      ),
      body: Column(
        children: [
          TextField(
+           controller: sample.state.text,
            maxLines: 3,
            minLines: 1,
          以下略
```

この場合、TextEditingControllerをstateとして管理することに煩わしさを感じるかどうかです

また、stateをcopyWithで置き換える際に、毎回新しいTextEditingControllerに置き換えることになるのですが、あまり必要な場面はなく、無駄になりやすい処理かなと思います（かといってあまり気にすることもないのですが）

# ③Notifierで管理する

NotifierにTextEditingControllerを定義する方法です

個人的にはこれが一番丸いのではないかと思っています

```diff_dart:sample_page_notifier.dart
class SamplePageNotifier extends StateNotifier<SamplePageState> {
  final Ref ref;

  SamplePageNotifier(this.ref) : super(SamplePageState());

+ TextEditingController _controller = TextEditingController();

  // 登録する
  void submit() {
    // 登録処理
  }
}
```

```diff_dart:sample_page.dart
class SampleView extends ConsumerWidget {
  const SampleView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final sample = ref.watch(sampleNotifierProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        title: const Text("サンプルアプリ"),
      ),
      body: Column(
        children: [
          TextField(
+           controller: sample._controller,
            maxLines: 3,
            minLines: 1,
          以下略
```

Notifier内に定義しておくことで、Notifier内の登録処理実行時にTextEditingControllerのtextを参照することができるので、処理もやりやすく、stateも同期しやすいと思います

# まとめ

正直②と③に関しては好みのところも大きいので、一概にどちらが良いとは言えないですが、TextEditingControllerの管理方法を決める際の手助けにならば幸いです

私自身、Flutter勉強中のみですので、記事に間違いや指摘事項等あれば教えていただけると幸いです

最後まで見ていただきありがとうございました

8日目の[TechCommit AdventCalendar2022](https://adventar.org/calendars/7673)もお楽しみに