FlutterでStateをもたせるには色々あるが、基本はStatefulWidgetを利用する。
StatefulWidgetは以下のようにStateを持ち、Stateの中で値を持ったりsetしたりできる。
StatefulWidget class - widgets library - Dart API
class Bird extends StatefulWidget { const Bird({ Key? key, this.color = const Color(0xFFFFE306), this.child, }) : super(key: key); final Color color; final Widget? child; @override _BirdState createState() => _BirdState(); } class _BirdState extends State<Bird> { double _size = 1.0; void grow() { setState(() { _size += 0.1; }); } @override Widget build(BuildContext context) { return Container( color: widget.color, transform: Matrix4.diagonal3Values(_size, _size, 1.0), child: widget.child, ); } }
他にもinitStateやdisposeなどWidgetが生成されるときや破棄されるタイミングに呼び出されるCallbackなどがある。
詳しくはドキュメントを読んでもらうとしてそんなのがある。
ちなみに逆にStatelessWidgetはStateを持たずinitStateなどもない。
StatefulWidgetの課題とは
initStateなどがあると言ったが、例えばテキストを入力できる画面を作りたいときにTextEditingControllerをinitStateで作って、disposeで破棄する処理を書いたりする。
TextEditingController class - widgets library - Dart API
/// Flutter code sample for TextEditingController // This example creates a [TextField] with a [TextEditingController] whose // change listener forces the entered text to be lower case and keeps the // cursor at the end of the input. import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); /// This is the main application widget. class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return const MaterialApp( title: _title, home: MyStatefulWidget(), ); } } /// This is the stateful widget that the main application instantiates. class MyStatefulWidget extends StatefulWidget { const MyStatefulWidget({Key? key}) : super(key: key); @override State<MyStatefulWidget> createState() => _MyStatefulWidgetState(); } /// This is the private State class that goes with MyStatefulWidget. class _MyStatefulWidgetState extends State<MyStatefulWidget> { final TextEditingController _controller = TextEditingController(); @override void initState() { super.initState(); _controller.addListener(() { final String text = _controller.text.toLowerCase(); _controller.value = _controller.value.copyWith( text: text, selection: TextSelection(baseOffset: text.length, extentOffset: text.length), composing: TextRange.empty, ); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Container( alignment: Alignment.center, padding: const EdgeInsets.all(6), child: TextFormField( controller: _controller, decoration: const InputDecoration(border: OutlineInputBorder()), ), ), ); } }
他にもScrollControllerだったりなんとかControllerがよくある。
そのたびに生成して破棄する処理を書く必要がある。
つまりTextEditingControllerに関する処理をinitStateやdisposeに書く必要があり関連するコードが一箇所にまとまらず分散してしまったり、何度も同じコードを書く必要がある。
解決方法1: mixinする
mixinする方法がある。
ただしmixinはTextEditingControllerが複数ある場合に対応出来ない。
解決方法2: Builderを用意する
TextEditingControllerBuilderみたいなWidgetを作って利用すればmixinの問題は解決するが、この方法はBuilderを使うのでbuilder外でControllerを使うことが難しくなる。
解決方法3: flutter_hooks
このように use*Controller
すればいいだけになる。解決方法1, 2で起きるような問題は起きない。
flutter_hooks | Flutter Package
class Example extends HookWidget { const Example({Key key, required this.duration}) : super(key: key); final Duration duration; @override Widget build(BuildContext context) { final controller = useAnimationController(duration: duration); return Container(); } }
ただし、flutter_hooksはそれなりにデメリットもあると感じている。
- HookWidgetを継承する形になり一般的なStatelessWidgetを継承する形ではない
- useEffectなどを多用するとbuildメソッドが大きくなってしまう
use*
は if の中で使う事はできないが今の所機械的に発見しにくい- Flutter公式が提供している機能ではない
イシューにはなっていないのか
イシューはある。
Static Metaprogrammingがあればいいみたいな話になっているがどうか
Reusing state logic is either too verbose or too difficult · Issue #51752 · flutter/flutter · GitHub
static metaprogramming自体導入されるかはまだ未定
language/intro.md at master · dart-lang/language · GitHub
hooksはFlutterに入らないのか
このイシューで色々と議論されたようですが今の所はなさそう。
Widget hooks · Issue #25280 · flutter/flutter · GitHub
まとめ
flutterh_ooksを使うのは良さげではあるのだが使う判断はまだ出来ておらずどうしたものかなーと思いながらStatefulWidgetを作っている。