Flutter: TextFieldを別クラスに配置すると、キーボード位置(viewInsets.bottom)を取得できない
結論:「resizeToAvoidBottomInset: false」を設定する
2022/11/24 Flutter エラー・バグ日記
TextFieldにフォーカスが当たってキーボードが出たとき、描画範囲オーバーのエラーを防ぐため、
MediaQuery.of(context).viewInsets.bottom
を用いて、キーボード上端の位置(縦幅)を把握し、TextFieldの縦幅を動的に調整していた。
しかし、TextFieldをScaffold内に直接配置せず、別クラスに分離したところ、この調整ができなくなってしまった。
Scaffold直下に配置していた場合
元々は以下のようなコードだった。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの)
// の名前の末尾には、大文字のオー「O」をつけています
// ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、
// 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// 画面の長さ情報のプロパティを定義(簡易化のためグローバル変数で定義)
late double screenHeightO; // 画面の縦幅
late double statusBarHeightO; // ステータスバーの高さ
late double appBarHeightO; // AppBarの高さ
const double containerHeightO = 50.0; // TextField上部のContainerの高さ
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test",
theme: ThemeData.light(),
// 変動することのない画面の縦幅、AppBarの高さ、ステータスバーの高さを先に取得しておく
builder: (BuildContext context, Widget? child) {
screenHeightO = MediaQuery.of(context).size.height;
statusBarHeightO = MediaQuery.of(context).padding.top;
appBarHeightO = AppBar().preferredSize.height;
return child!;
},
home: SampleScreen(),
);
}
}
class SampleScreen extends StatefulWidget {
@override
_SampleScreenState createState() => _SampleScreenState();
}
class _SampleScreenState extends State<SampleScreen> {
@override
Widget build(BuildContext context) {
debugPrint("全体を描画");
// 動的にキーボードの位置(縦幅)を取得
double textFieldBottomO = MediaQuery.of(context).viewInsets.bottom;
debugPrint("textFieldBottomO = $textFieldBottomO");
return Scaffold(
appBar: AppBar(
title: Text("TestApp"),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
child: Text("TextField"),
alignment: Alignment.center,
color: Colors.green[200],
height: containerHeightO,
),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
// TextFieldの高さを計算して算出
// 画面の縦幅 - ステータスバーの高さ - AppBarの高さ - 上下パディング - 上部Containerの高さ - キーボードの縦幅
height: screenHeightO - statusBarHeightO - appBarHeightO - 8.0 * 2 - containerHeightO - textFieldBottomO,
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
),
keyboardType: TextInputType.multiline,
maxLines: null,
),
),
],
),
),
);
}
}
画面の縦幅から、ステータスバーの高さ、AppBarの高さ、上下パディング、上部Containerの高さ、キーボードの縦幅を差し引くことで、TextFieldの縦幅を計算している。
キーボードが出現すると、「MediaQuery.of」メソッドが呼び出されて、
キーボードの縦幅 = MediaQuery.of(context).viewInsets.bottom
を取得し、TextFieldの縦幅が再計算・再描画される仕様。
※下記公式サイトに説明があるとおり、「MediaQuery.of」メソッドを発動すると、buildメソッドが再実行(Widgetを再描画)される。
下位クラスに分離した場合
ただ、上記コードだと、キーボードが出るたびに、「Scaffold」以下の全てのWidgetが再描画されてしまう。
そこで、再描画範囲を限定するために、TextField部分を別クラスに切り出し、「MediaQuery.of」メソッドも別クラス側に配置することにした。
しかし、、この方法でビルドすると、キーボード出現時にエラーになってしまった。

コンソールで数値を確認したところ、「viewInsets.bottom」の値が「0」のままで、キーボード縦幅を取得できていない模様。
実は以前から解決策の情報があった模様(GitHub、StackOverflow)
調べたところ、下記に情報があった。もしかすると常識だったのかもしれない。。
Scaffoldの「resizeToAvoidBottomInset」プロパティを「false」に設定すれば良いとのこと(デフォルトだと「true」に設定されている)。
スクリーンキーボードとWidgetが重ならないように、サイズ調整するか否かを決めるプロパティらしい。
確かに、この1行を入れるだけで、別クラスに配置したTextFieldであっても、その縦幅をキーボード位置に応じて変化できるようになった。
追記した後の全コードは以下のとおり。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
late double screenHeightO;
late double statusBarHeightO;
late double appBarHeightO;
const double containerHeightO = 50.0;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test",
theme: ThemeData.light(),
builder: (BuildContext context, Widget? child) {
screenHeightO = MediaQuery.of(context).size.height;
statusBarHeightO = MediaQuery.of(context).padding.top;
appBarHeightO = AppBar().preferredSize.height;
return child!;
},
home: SampleScreen(),
);
}
}
class SampleScreen extends StatefulWidget {
@override
_SampleScreenState createState() => _SampleScreenState();
}
class _SampleScreenState extends State<SampleScreen> {
@override
Widget build(BuildContext context) {
debugPrint("全体を描画");
return Scaffold(
/// ↓これを追加する ※これがないと、viewInsets.bottom の値を取得できない
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("TestApp"),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
child: Text("TextField"),
alignment: Alignment.center,
color: Colors.green[200],
height: containerHeightO,
),
// TextField部分を別クラス化
TextFieldWidgetO(),
],
),
),
);
}
}
/// 別クラス化した TextField を含むウィジェット
class TextFieldWidgetO extends StatefulWidget {
const TextFieldWidgetO({Key? key}) : super(key: key);
@override
State<TextFieldWidgetO> createState() => _TextFieldWidgetOState();
}
class _TextFieldWidgetOState extends State<TextFieldWidgetO> {
@override
Widget build(BuildContext context) {
debugPrint("TextFieldWidgetOを描画");
// 動的にキーボードの位置(縦幅)を取得
double textFieldBottomO = MediaQuery.of(context).viewInsets.bottom;
debugPrint("textFieldBottomO = $textFieldBottomO");
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
// TextFieldの高さを計算して算出
// 画面の縦幅 - ステータスバーの高さ - AppBarの高さ - 上下パディング - 上部Containerの高さ - キーボードの縦幅
height: screenHeightO - statusBarHeightO - appBarHeightO - 8.0 * 2 - containerHeightO - textFieldBottomO,
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
),
keyboardType: TextInputType.multiline,
maxLines: null,
),
);
}
}
(考察)contextを渡しても解消するが、全体が再描画されてしまう
以前、下記日記にあるとおり、「Scaffold」の下位クラスだと、ステータスバーの高さを示す「padding.top」を取得できなかった。
このときは、contextを下位クラスに引数で渡すことで解消できた。
そのため、「viewInsets.bottom」が取得できない原因も同じだと考え、contextを引数として「TextFieldWidgetO」に渡してみたところ、予想どおり「resizeToAvoidBottomInset: false」の設定をせずとも、問題を解消できた。
しかし、この方法だと、「Scaffold」直下のbuildメソッドも回ってしまい、画面全体が再描画されてしまった。
そのため、「resizeToAvoidBottomInset: false」を用いる方がシンプルで、描画効率も良いと思われる。
\一般的なエラー対処法をまとめた記事はこちら/
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/







