手で文字を打てて、ボタンでコードから文字を入れたり全部消したりでき、文字数もリアルタイムで表示される入力欄を、TextField と TextEditingController で作れます。

アプリの入力欄は、手で打った文字を読むだけでなく、プログラムから文字を入れたり、中身が変わるたびに何かを表示したりしたい場面がよくあります。
Flutter では、入力欄の部品 TextField に、中身を操作するための TextEditingController を組み合わせると、どちらもできます。
本記事では、サンプル文を入れる、全部消す、文字数を表示する、という3つの動きを持つ入力欄を作りながら、TextField と TextEditingController の使い方を説明します。
- Flutter で入力欄(テキストボックス)を作りたい人
- 入力された文字を、コードから読み取りたい人
- ボタンなどで、コードから入力欄の文字を書き換えたい人
- TextEditingController を、なぜ後片付けしなければいけないのか知りたい人
TextField(テキスト入力欄)

TextField は、文字を入力するための入力欄の部品です。最小の書き方は次のとおりです。
const TextField()見た目や動きは、プロパティで変えます。この記事で使うものは次の3つです。
| プロパティ | 役割 |
|---|---|
| controller | 入力欄の中身を操作する TextEditingController を渡す |
| maxLines | 表示する行数。2以上にすると複数行の入力欄になる |
| decoration | 枠線や、何も入力されていないときの案内文(hintText)を決める |
参考:TextField(Flutter公式)
TextEditingController(入力欄の中身を操作する)

TextEditingController は、入力欄の中身を読んだり書いたりするための部品です。TextField の controller に渡すと、2つの部品がつながります。
final TextEditingController _controller = TextEditingController();
TextField(controller: _controller)つなげたあとは、次のことができます。
| やりたいこと | 書き方 |
|---|---|
| いまの文字を読む | _controller.text |
| 文字を書き換える | _controller.text = ‘新しい文字’ |
| 全部消す | _controller.clear() |
| 中身が変わったことを知る | _controller.addListener(…)、またはウィジェットで監視する |
入力された文字を取る方法は、TextEditingController 以外にもあります。それぞれ、呼ばれるタイミングが違います。
| 方法 | タイミング |
|---|---|
| onChanged | 1文字入力するたびに呼ばれる |
| onSubmitted | 入力を終えたとき(キーボードの確定ボタンなど)に呼ばれる |
| TextEditingController | 好きなタイミングで text を読める。コードから書き換えもできる |
読むだけなら onChanged や onSubmitted でも足ります。この記事のように、コードから文字を入れたり、文字数を出し続けたりするなら、TextEditingController が向いています。
この記事では、次の動きをする入力欄を作ります。
- 手で文字を入力できる(複数行)
- 入力欄の下に、いまの文字数が出る
- 「サンプルを入れる」ボタンで、コードから文章が入る。カーソルは文字の最後に置く
- 「全部消す」ボタンで、入力欄が空になる
ソースコード
- text_input_page.dart
入力欄、文字数の表示、2つのボタンを持つ画面です。
import 'package:flutter/material.dart';
class TextInputPage extends StatefulWidget {
const TextInputPage({super.key, required this.title});
final String title;
@override
State<TextInputPage> createState() => _TextInputPageState();
}
class _TextInputPageState extends State<TextInputPage> {
static const _sampleText = 'Hello, nice to meet you.';
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
/// コードから文字を入れ、カーソルを文字の最後に置く。
void _insertSample() {
_controller.text = _sampleText;
_controller.selection = TextSelection.collapsed(offset: _sampleText.length);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _controller,
maxLines: 6,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '直接入力してください',
),
),
const SizedBox(height: 12),
// 中身が変わるたびに、文字数を作り直す。
ListenableBuilder(
listenable: _controller,
builder: (context, _) => Text('${_controller.text.length} 文字'),
),
const SizedBox(height: 12),
Row(
children: [
FilledButton(
onPressed: _insertSample,
child: const Text('サンプルを入れる'),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: _controller.clear,
child: const Text('全部消す'),
),
],
),
],
),
),
);
}
}- main.dart
動かすための最小の入口です。text_input_page.dart は lib/pages/ に置きます。
import 'package:flutter/material.dart';
import 'pages/text_input_page.dart';
void main() {
runApp(
const MaterialApp(home: TextInputPage(title: 'テキスト入力')),
);
}解説
画面は StatefulWidget で作っています。TextEditingController は、画面が表示されている間ずっと同じものを使い続けるので、State のフィールドとして1つだけ作ります。
使っている部品は、入力欄の TextField、文字数を出す ListenableBuilder、操作する2つのボタンです。
ListenableBuilder:文字数を更新し続ける
文字数の Text は ListenableBuilder で包んでいます。listenable に TextEditingController を渡すと、中身が変わるたびに builder の中が作り直されます。
TextEditingController は、中身が変わると監視している側に知らせる仕組みを持っています(変化を知らせる部品の仲間として作られています)。ListenableBuilder は、公式の説明では、監視している対象が変わったときに、その部分の画面を作り直す部品で、builder は変化の知らせが来るたびに呼ばれます。ここでは、その知らせを受け取って、文字数の表示だけを更新します。
onChanged の中で setState を呼んでも文字数は出せます。ただ、その場合は画面全体が作り直されます。ListenableBuilder なら、作り直す範囲を文字数の部分だけに絞れます。
_insertSample:コードから文字を入れる
_controller.text = _sampleText; で、入力欄の文字が置き換わります。
ただし、公式の説明では、text を書き換えると、選択範囲や変換中の範囲が消えます。文字を入れたあと、カーソルをどこに置くかも決まらなくなります。
そこで、次の行でカーソルを文字の最後に置いています。
_controller.selection = TextSelection.collapsed(offset: _sampleText.length);TextSelection.collapsed は、範囲を選ばず、1か所にカーソルを置く指定です。offset に文字数を渡すと、文字の最後になります。
文字とカーソルを同時に決めたいときは、公式の説明のとおり、value に TextEditingValue をまとめて入れる方法もあります。
clear:全部消す
_controller.clear は、入力欄を空にする関数です。ボタンの onPressed に、そのまま渡せます。
dispose:後片付け
TextEditingController は、使い終わったら dispose で後片付けします。公式の説明にも、不要になったら dispose する、と書かれています。画面が閉じるときに呼ばれる State の dispose の中で、_controller.dispose() を呼びます。
エラーと解決策・対策
以下では、公式の説明で注意されている、つまずきやすい点と対処を紹介します。実際に起きたエラーではなく、起こりやすい点をまとめたものです。
文字を入れたのに、カーソルが思った場所にない
_controller.text だけを書き換えた場合、選択範囲が消えるため、カーソルの位置が思った場所にならないことがあります。上のコードのように selection も指定するか、value にまとめて入れてください。
入力のたびに、変な動きや無限ループになる
addListener の中で、同じ TextEditingController の text を書き換えると、書き換えがまた知らせとして届くため、無限に繰り返すおそれがあります。公式の説明でも、この点が注意されています。入力内容を整えたいときは、入力時に文字を変換する TextInputFormatter を使ってください。
まとめ
TextField は入力欄の部品、TextEditingController はその中身を操作する部品です。
- controller に渡すと、text で読み書きできる
- text を書き換えると選択範囲が消えるので、カーソルの位置は selection で決める
- 中身の変化に合わせて表示を変えるなら、ListenableBuilder で作り直す範囲を絞れる
- 使い終わったら dispose で後片付けする

