19
7

More than 1 year has passed since last update.

【Flutter】RiverpodでTextEditingControllerを使いたい

Posted at

はじめに

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

この記事はTechCommit AdventCalendar2022の7日目の記事です。

昨日はにしやまさんのGoでdiffコマンド書いて実際の実装と比べてみたでした

概要

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

今回の題材はTextEditingControllerです。

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

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

構成

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

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で管理しています

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

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

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を管理することも可能です

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に渡してあげる必要があります

sample_page.dart
        TextButton(
-           onPressed: () {sample.submit()},
+           onPressed: () {sample.submit(_controller.text)},
            child: const Text('登録'),
          ),
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として保持する方法です

sample_page_state.dart
@freezed
class SamplePageState with _$SamplePageState {
  const factory SamplePageState({
-    @Default("") String text,
+    TextEditingController text,
  }) = _SamplePageState;
}
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を定義する方法です

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

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

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

+ TextEditingController _controller = TextEditingController();

  // 登録する
  void submit() {
    // 登録処理
  }
}
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もお楽しみに

19
7
1

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
19
7