【Flutter】StatefulWidget の2つの問題点。どうして実践ではあまり使われないか?その代わりにどうすればいいか?
Flutter を学習するとまず StatefulWidget と StatelessWidget を勉強することになると思います。チュートリアルなどでも setState などの StatefulWidget の使い方を学びます。しかし、実際のアプリ開発の現場ではあまり StatefulWidget は使われません。それはなぜか、また、代わりにどうしたらいいかについて解説してしていきます。まず、StatefulWidget の 2つの問題点を挙げます。
1. 離れたツリー間で連絡するのが困難
この問題が一番クリティカルです。ここで連絡すると言っているのは、状態を参照する、状態を変更する、状態を初期化するなどの処理のことです。
1画面で完結している画面なら、StatefulWidget で何の問題もないのですが、ツリー上で離れた位置にある Widget のあいだで、 Widget の状態を変更したり、ビジネスロジックを呼び出したりする時に問題が発生します。
StatefulWidget だけでこれを実現するのはとても困難で例えばグローバル変数を定義したりする必要が出てきます(それはやめましょう)。
例えば、ログインユーザーのデータを考えてみましょう。ログイン画面でユーザーデータを取得します。アプリのいろんなところからアクセスしたいデータです。このデータを他の Widget が使うことを考えてみましょう。こう言った時に StatefulWidget ではうまく実装できません。
2. メンテナンスしにくい、テストしにくい
StatefulWidget だけを使ってロジックを書くことでメンテナンス性が下がることも挙げられます。状態を変更する処理やそれに伴うロジックを画面に書くと、コードがごちゃごちゃしてしまいます。画面は受け取った情報を表示するだけ、ロジックや状態からは画面にはアクセスしないようにすることでメンテナンス性が向上します。
また、コードが長くなる点が挙げられます。これは State Widget を別個作る必要ある点、リビルドするのに setState を呼ぶ必要があるためです。また無理に下位 Widget に状態を伝搬させるコストもコードを複雑にしてメンテナンス性が失われます。
StatefulWidget で実装すると、画面と状態やロジックが密接に絡み合っているため、これらの部分を切り出してテストすることが困難になります。ロジックと状態を個別に切り出すことで簡単に、ロジックのテストと言ったことができるようになります。
代わりにどうすればいいか
これまでの Flutter の状態管理の歴史ですが、StatefulWidget → ScopedModel → BLoC → provider → riverpod という大まかな流れがあります。Flutter や React などのフロントエンド界隈は移り変わりが激しいですが、最新の riverpod と flutter_hooks を活用した方法はこれ以上コードを短くするのが困難なレベルなので、ほとんど完成形だと思われます(と言いつつまた新しい物が流行る可能性はあります)。
参考までに、カウンターアプリを例に最新の状態管理法と StatefulWidget を比較をしてみましょう。
まずこちらが一般的な StatefulWidget
class CounterScreen extends StatefulWidget {
@override
_MyHomePageState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
var _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('$_count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_count++;
});
},
),
);
}
}
こちらが riverpod + flutter_hooks を使用した方法です。
final counter = StateProvider((ref) => 0);
class CounterScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final count = useProvider(counter).state;
return Scaffold(
body: Center(child: Text('${count}')),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(counter).state++;
},
),
);
}
}
State ウィジェットを作る必要はないため、短く書けることがわかります。
しかし、riverpod で実装するメリットはコードの短さだけではなく以下の恩恵があります。
✅ どこからでも状態にアクセスできる
✅ どこからでも状態を変更できる
✅ 状態だけを取り出してテストすることができる
riverpod の問題点を強いて挙げるなら、新たに学習する必要がある、riverpod パッケージ周りを追う必要がある、ぐらいだと思います。
riverpod の概要についてはこちらにまとめています:
riverpod を使ったアーキテクチャについてはこちらを参考にしてください: