はじめに
突然ですが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
に前述の列挙型を渡して更新のタイミングを指定します。onSubmitted
とonChanged
では指定された処理の前に更新を行います。なるべく元の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
をより使いやすくする方法について紹介しました。とはいえTextField
とStateNotifier
が密結合になって自由度は多少下がります。お好きに使ってください。