Flutter: React でいう制御されたコンポーネントの実装

Flutter: React でいう制御されたコンポーネントの実装

September 13, 2022

インプットフォームを作成する際、React では「制御されたコンポーネント(Controlled Components)」と「非制御コンポーネント(Uncontrolled Components)」という2つの考え方があります。

特性を一言で表すならば、制御されたコンポーネントは扱いが容易でバグが入り込みづらく、非制御コンポーネントは扱いが複雑になりやすいが処理性能は向上します。

Flutter でインプットフォームを作る場合、通常はそのインプットフォーム自身が状態を保持する(いまどのような文字列が入力されているか?など)ため、これは「非制御コンポーネント」です。

Flutter で制御されたコンポーネントの実装 #

Flutter での制御されたコンポーネントは以下のような実装で実現できます。

import 'package:flutter/material.dart';

class TextFieldWidget extends StatefulWidget {
  const TextFieldWidget({
    required this.text,
    required this.onChanged,
    super.key,
  });

  final String text;
  final void Function(String) onChanged;

  @override
  State<TextFieldWidget> createState() => _TextFieldWidgetState();
}

class _TextFieldWidgetState extends State<TextFieldWidget> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final TextSelection previousSelection = _controller.selection;
    _controller.text = widget.text;
    _controller.selection = previousSelection;

    return TextFormField(
      controller: _controller,
      onChanged: widget.onChanged,
    );
  }
}

説明 #

Widget build() の部分について説明します。

@override
Widget build(BuildContext context) {
  final TextSelection previousSelection = _controller.selection;
  _controller.text = widget.text;
  _controller.selection = previousSelection;

  return TextFormField(
    controller: _controller,
    onChanged: widget.onChanged,
  );
}

次の部分で、プロップスで受け取った textTextEditingControllertext フィールドに設定しています。

_controller.text = widget.text;

これによりこのウィジェット(コンポーネント)の使用者側で保持している text の値をインプットフォームに反映することができます。

そして、その前後にある TextSelection の記述はカーソル位置を調整するためのものです。

final TextSelection previousSelection = _controller.selection;

_controller.selection = previousSelection;

このウィジェットは text の値が変わるたびにリビルドされます。このとき入力カーソルの位置が先頭に戻ってしまいます。

つまりそのままだと1文字入力するたびにカーソルが先頭に戻ることになりますが、これを防ぐための記述です。

補足 #

リセットボタンなどで text の中身を一気にクリアしたとき _controller.selection = previousSelection のところでエラーになります。

これを防ぎつつ正常に挙動させるためには次のようにします。

@override
Widget build(BuildContext context) {
  final TextSelection previousSelection = _controller.selection;
  _controller.text = widget.text;
  try {
    _controller.selection = previousSelection;
  } on FlutterError {
    _controller.selection = TextSelection.fromPosition(
      TextPosition(offset: _controller.text.length),
    );
  }

  return TextFormField(
    controller: _controller,
    onChanged: widget.onChanged,
  );
}

ご覧の通りなかなか無理矢理なコードです。Flutter では基本通りに非制御コンポーネントを使った方が良いかもしれませんね。