2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter TextEditingController×Riverpodの改善

Last updated at Posted at 2025-02-27

はじめに

突然ですがFlutterのTextEditingControllerってぶっちゃけ中途半端じゃないですか!?例として簡単なテキストフィールドを用いたページを示します。

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

final textControllerProvider = StateProvider((ref) => TextEditingController());
final textProvider = StateProvider<String>((ref) => '');

class SamplePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final textController = ref.watch(textControllerProvider);
    final text = ref.watch(textProvider);

    return Scaffold(
      body: Column(
        children: [
          TextField(
            controller: textController,
            onSubmitted: (value) {
              ref.read(textProvider.notifier).state = value;
            },
          ),
          Text('You typed: $text'),
        ],
      ),
    );
  }
}

個人的にせっかくTextEditingControllerを使ったのに、もう一個テキストの状態を用意して送信(onSubmitted)のタイミングで更新するのはバカらしいじゃないですかと思うわけです。せいぜいテキストフィールドとしての機能の半分ぐらいしかコントロールできてないじゃん、と。
なら直接TextEditingController.textを見ればいいじゃないかと思うわけですが、

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

final textControllerProvider = StateProvider((ref) => TextEditingController());

class SamplePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final textController = ref.watch(textControllerProvider);

    return Scaffold(
      body: Column(
        children: [
          TextField(
            controller: textController,
          ),
          Text('You typed: ${textController.text}'),
        ],
      ),
    );
  }
}

これでは反応しないんですよね。StateProviderから見た時に"状態"はTextEditingController自体であってその内部の.textではないからだと思います。
finalをつけた時そのオブジェクトを再代入することはできませんがメソッド経由で値を書き換えることができるのと同じかな。

改善策

自作ライブラリみたいなノリで作ってみました。正式なライブラリにするつもりは今のところないのでコピペして使ってください。

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class TextEditingNotifier extends StateNotifier<String> {
  TextEditingNotifier([super.value = ''])
      : _controller = TextEditingController(text: value);

  final TextEditingController _controller;
  TextEditingController get controller => _controller;

  void update() {
    state = _controller.text;
  }

  void clear() {
    _controller.clear();
  }
}

enum TextEditingUpdateType { onSubmitted, onChanged, never }

class NotifierTextField extends TextField {
  NotifierTextField({
    required this.notifier,
    TextEditingUpdateType updateType = TextEditingUpdateType.onSubmitted,
    super.key,
    super.focusNode,
    super.decoration,
    super.keyboardType,
    super.textInputAction,
    super.textCapitalization,
    super.style,
    super.strutStyle,
    super.textAlign,
    super.textAlignVertical,
    super.textDirection,
    super.readOnly,
    super.showCursor,
    super.autofocus,
    super.obscureText,
    super.autocorrect,
    super.smartDashesType,
    super.smartQuotesType,
    super.enableSuggestions,
    super.maxLines,
    super.minLines,
    super.expands,
    super.maxLength,
    super.maxLengthEnforcement,
    super.onEditingComplete,
    void Function(String)? onSubmitted,
    super.inputFormatters,
    super.enabled,
    super.cursorWidth,
    super.cursorHeight,
    super.cursorRadius,
    super.cursorColor,
    super.selectionHeightStyle,
    super.selectionWidthStyle,
    super.keyboardAppearance,
    super.scrollPadding,
    super.dragStartBehavior,
    super.enableInteractiveSelection = true,
    super.selectionControls,
    super.onTap,
    void Function(String)? onChanged,
    super.onTapOutside,
    super.mouseCursor,
    super.buildCounter,
    super.scrollController,
    super.scrollPhysics,
    super.autofillHints = null,
    super.contentInsertionConfiguration,
    super.clipBehavior,
    super.restorationId,
    super.stylusHandwritingEnabled,
    super.enableIMEPersonalizedLearning,
  }) : super(
          controller: notifier.controller,
          onSubmitted: (text) {
            if (updateType == TextEditingUpdateType.onSubmitted) {
              notifier.update();
            }
            if (onSubmitted != null) {
              onSubmitted(text);
            }
          },
          onChanged: (text) {
            if (updateType == TextEditingUpdateType.onChanged) {
              notifier.update();
            }
            if (onChanged != null) {
              onChanged(text);
            }
          },
        );
  final TextEditingNotifier notifier;
}

class NotifierSubmitButton extends ElevatedButton {
  NotifierSubmitButton({
    required this.notifier,
    void Function()? onPressed,
    super.onLongPress,
    super.onHover,
    super.onFocusChange,
    super.style,
    super.focusNode,
    super.autofocus = false,
    super.clipBehavior,
    super.statesController,
    super.child,
  }) : super(
          onPressed: () {
            notifier.update();
            if (onPressed != null) {
              onPressed();
            }
          },
        );

  final TextEditingNotifier notifier;
}

TextEditingNotifier

Controllerに相当します。状態をStringにして内部にTextEditingControllerを持たせます。これにより内部でstate = _controller.textを行うことでアプリ全体に状態の変更が通知されます。StateNotifierProviderでラップして使うことになります。(後述)

TextEditingUpdateType

後述のテキストフィールドで使う更新のタイミングを制御するための列挙型です。変更時、送信時、無効の3種類が選べます。別で送信ボタンがあるときなどは無効にします。

NotifierTextField

今回専用のテキストフィールドです。TextFieldを継承しますが、controllerの代わりにnotifierを指定し、updateTypeに前述の列挙型を渡して更新のタイミングを指定します。onSubmittedonChangedでは指定された処理の前に更新を行います。なるべく元のTextFieldの属性も引き継げるようにしましたが、もし引数が足りてなかったら自分で足してください。

NotifierSubmitButton

送信ボタン用です。notifierを指定することで押下時に勝手に更新してくれます。

使い方

送信ボタンの有無で2つ作ったのでぱっと見改善前よりコードが増えてるように見えますがそんなことないです。TextEditingProviderはNotifierでラップしてref.watchで参照します。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_sample/shared/custom_widgets.dart';

//送信ボタンなし用Provider
final textEditingNotifierProvider1 =
    StateNotifierProvider<TextEditingNotifier, String>(
  (ref) => TextEditingNotifier(),
);
//送信ボタンあり用Provider
final textEditingNotifierProvider2 =
    StateNotifierProvider<TextEditingNotifier, String>(
  (ref) => TextEditingNotifier(),
);

class SamplePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final notifier1 = ref.watch(textEditingNotifierProvider1.notifier);
    final notifier2 = ref.watch(textEditingNotifierProvider2.notifier);
    return Scaffold(
      body: Column(
        children: [
          //送信ボタンなし
          NotifierTextField(
            notifier: notifier1,
          ),
          Text('You typed: ${ref.watch(textEditingNotifierProvider1)}'),
          //送信ボタンあり
          NotifierTextField(
            notifier: notifier2,
            updateType: TextEditingUpdateType.never,
          ),
          NotifierSubmitButton(
            notifier: notifier2,
            child: const Text('送信'),
          ),
          Text('You typed: ${ref.watch(textEditingNotifierProvider2)}'),
        ],
      ),
    );
  }
}

唯一の問題は状態(String)が欲しい時とNotifierが欲しい時でref.watch()から重複するところでしょうか。Riverpodの使用上致し方ないところだと思いますが改善策があればコメントください。

まとめ

今回はTextEditingControllerをより使いやすくする方法について紹介しました。とはいえTextFieldStateNotifierが密結合になって自由度は多少下がります。お好きに使ってください。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?