さて、FlutterのStateとクラスについての学習記録
その3です。
今回はついにStateを導入したいと思います。
しかし、
前回までの記事で、なんとなく
Stateの役割は分かったのですが
本当に、Stateがないとダメなの?
って、まだちょっと疑っています。
そんなわけで、まずは
Stateがないとどうなるのか
実験してみることにしました。
Step1 Stateなしの場合
実験台にするWidgetはTextFieldです。
まずは、簡単なtextFieldDefinitionを用意しました。
※◯◯Definitionというのは
私が開発中のアプリWidget Labで
各Widgetごとに作成しているファイルです。
Touchモードで触れるパラメータ
リアルタイムプレビューに表示されるWidget
などについての設定が書かれています。
コードはこちら
final textFieldDefinition = WidgetDefinition(
id: 'textField',
title: 'TextField',
category: 'Input',
parentId: 'StatefulWidget',
description: 'ユーザーがテキストを入力できるWidget',
params: [],
touchParams: [
TouchParam(
key: 'text',
uiType: TouchUiType.text,
label: 'Text',
initialValue: '',
),
TouchParam(
key: 'hintText',
uiType: TouchUiType.text,
label: 'Hint Text',
initialValue: '',
),
TouchParam(
key: 'width',
uiType: TouchUiType.slider,
label: 'Width',
initialValue: 240.0,
min: 100,
max: 400,
),
TouchParam(
key: 'enabled',
uiType: TouchUiType.switchButton,
label: 'Enabled',
initialValue: true,
),
TouchParam(
key: 'readOnly',
uiType: TouchUiType.switchButton,
label: 'Read Only',
initialValue: false,
),
],
previewBuilder: (values) {
final width =
(values['width'] ?? 240.0).toDouble();
final text =
values['text'] ?? '';
final hintText =
values['hintText'] ?? '';
final enabled =
values['enabled'] ?? true;
final readOnly =
values['readOnly'] ?? false;
return SizedBox(
width: width,
child: TextField(
controller: TextEditingController(
text: text,
),
decoration: InputDecoration(
hintText: hintText,
),
enabled: enabled,
readOnly: readOnly,
),
);
},
miniPreviewBuilder: () {
return const SizedBox(
width: 120,
child: TextField(
decoration: InputDecoration(
hintText: 'TextField',
),
),
);
},
codeBuilder: (values) {
final hintText =
values['hintText'] ?? '';
final enabled =
values['enabled'] ?? true;
final readOnly =
values['readOnly'] ?? false;
return '''
TextField(
decoration: InputDecoration(
hintText: '$hintText',
),
enabled: $enabled,
readOnly: $readOnly,
)
''';
},
);
previewBuilderという関数が
プレビューに表示されるWidgetを返します。
ユーザーが調整した設定値を
valuesで渡しています。
今まで作成したStatelessWidgetのページは
この形式で作っていて
何も問題はありませんでした。
さて、StatefulWidgetであるTextFieldでは
どんな問題が起こるのでしょうか?
テキストボックス内に文字を入力して
パラメータをいじると
入力した文字が消えてしまいます。
なるほど、ChatGPTの言っていたとおりです。
これでは
長い文字を入力した時の挙動を試してみよう
なんて使い方ができません。
Stateを導入する方法
さて、Stateの必要性は理解できました。
どうやったらStateを導入できるのでしょうか?
ChatGPTに、必要な作業をチェックボックス形式で出してもらうと
こうなりました。
PreviewStateを作る
-
PreviewStateクラスを新規作成 -
TextEditingControllerを持たせる
TouchPageを修正する
-
PreviewStateを保持する変数を追加 -
initState()でPreviewStateを生成する
WidgetDefinitionを修正する
-
previewBuilderの引数を変更するpreviewBuilder(values)- ↓
previewBuilder(values, state)
呼び出し側を修正する
-
previewBuilder(values)を呼んでいる箇所をすべて修正
これを見て、私は思いました。
TextFieldを1つ追加したいだけなのに、結構大工事だな……。
特に最後のところ。
今までに作ったWidgetのページはできればいじりたくない。
もっと楽な方法ないのかな?
そこで思いついたのが、
StateをValuesの中に入れてしまう
という方法でした。
ChatGPTに聞いたところ、
それでも一応動くよ、とのことなので
試してみることにしました。
Step2 StateをVvaluesに入れた場合
こんな感じになりました。
意外とスッキリしているし、
変更箇所もここだけで済みそうです。
コードはこちら
previewBuilder: (values) {
final width =
(values['width'] ?? 240.0).toDouble();
final hintText =
values['hintText'] ?? '';
final enabled =
values['enabled'] ?? true;
final readOnly =
values['readOnly'] ?? false;
// ----------------------------
// StateをValuesの中に保存
// ----------------------------
values['_state'] ??= {
'controller': TextEditingController(
text: values['text'] ?? '',
),
};
final state =
values['_state'] as Map<String, dynamic>;
final controller =
state['controller']
as TextEditingController;
return SizedBox(
width: width,
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText,
),
enabled: enabled,
readOnly: readOnly,
),
);
},
そして実行結果がこちら。
State→Widgetへの情報伝達や更新が
リアルタイムにできないのでは?と心配でしたが
何も問題ありませんでした。
どうやらこの方法でも
Stateは機能するみたいです。
さらに、入力後に
戻るボタンで別のページに戻ってから
再度このページを表示したら
ちゃんと最初の状態に戻っていました。
一度入力した値がずっと残ってしまうのでは?
と心配でしたが、それも問題ないようです。
Step3 Stateをしっかり実装する
さて、Step2の方法でも
用が足りたのですが
ChatGPT的には
Stateとvaluesは別々に渡すのが
断然オススメみたいです。
今後、コードプレビューを実装する場合に
Step2の方法だと
values.remove('_state');
のような除外処理を毎回書く必要があるかも…
みたいなことを言われて
これってつまり、
今、楽をするか
これから楽をするか
の違いなのだろうな、と思いました。
せっかくなので、勉強のためにも
ちゃんとした実装をしてみることにしました。
手順は上で示したとおりです。
新しく作ったのは、このファイルぐらい。
class PreviewState {
TextEditingController? textController;
}
そして、previewBuilderはこんな感じになりました。
コードはこちら
previewBuilder: (
values,
previewState,
) {
final width =
(values['width'] ?? 240.0).toDouble();
final hintText =
values['hintText'] ?? '';
final enabled =
values['enabled'] ?? true;
final readOnly =
values['readOnly'] ?? false;
previewState.textController ??=
TextEditingController(
text: values['text'] ?? '',
);
final controller =
previewState.textController!;
return SizedBox(
width: width,
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText,
),
enabled: enabled,
readOnly: readOnly,
),
);
},
意外とすんなり作業は完了しました。
そして、実行結果はStep2と全く同じでした。
まとめ
正直、Stateを扱うコードそのものについては
まだ充分に理解していません。
それでも、以前はわけがわからなくて
途方もない作業に感じた、Stateの導入が
無事達成できたことは非常にうれしかったです。
これからは、Widget Labに
StatefulWidgetのページを
どんどん追加していきたいと思います。
