Flutterのアニメーションシステムに頼らず一定間隔でWidgetを動かす?
宣言的な書き方でゲームを作ってみるというクレイジーなことをしているときのメモ.
最初は画面更新+描画のきっかけ(要はクロック,普通のエンジンで毎フレーム叩かれる関数を実現する方法)はAnimationControllerでやることになるんじゃないかなと思ってた.
しかしある時,ふとこんなことを考えた.
Flutterでアニメーションする場合,特定の状況ではアニメーションシステムに乗っかるのではなく自前でクロックを生成するStreamを定義したほうがやりやすい説
というのも本当に毎フレーム描画したいならそりゃAnimationController使って無条件にsetStateすれば良いんだけど,今回作ってるゲームは別にそこまでする必要がなくて,あくまで状態が更新されたときだけでいい.
というか宣言的っていうくらいならそうじゃないとうまみがないじゃん?
というわけで,一定間隔で何らかの信号を発するStreamを定義して,それをリビルドのきっかけにしようと思ったのでやってみた.
code:dart
final clockProvider = StreamProvider.autoDispose<int>((ref) async* {
final baseStream =
Stream<int>.periodic(const Duration(milliseconds: 200), (c) => c);
bool _enabled = true;
ref.onDispose(() => _enabled = false);
await for (final value in baseStream) {
if (_enabled) yield value;
}
});
Riverpodを使う前提で,こんなProviderを用意する._enabledは,periodicなStreamを止める方法がよくわからなかったので苦肉の策.
そしてこれを任意のConsumerWidgetで参照するだけ.
code:dart
@override
Widget build(BuildContext context, ScopedReader watch) {
final clock = watch(clockProvider);
return clock.when(
data: (value) {
...
},
...
);
}
一定間隔でリビルドされるようになったよ!やったね!
ちょっと待って! それ,StateNotifierでよくね?
シャワー浴びてるときに気づいた.
状態が変わったときだけ描画したいって,つまりStateNotifierでいいじゃん.
StateNotifier継承クラス(Controller)の内部でperiodicなStreamを持っておいて,listenメソッドでstateを更新する感じにすれば,大体同じことができるはず.
ここまで考えたものの,この方法の欠点を見つけた.
一定間隔で更新したい状態が複数ある場合,それもその間隔がそれぞれ違う場合,描画の間隔がそれぞれのクロックを合成したものになる.
この方法だと,実装としては複数のStreamの購読内でnotifyListeners()が走ることになる.
そこで複数の状態の更新タイミングが一致した場合を考えると,最悪の場合おなじみのエラーが見えることになる.
The following assertion was thrown building Builder:
setState() or markNeedsBuild() called during build.
結論
どこにStreamを置くかは場合によって使い分けるのが吉(それはそう).
個人的にはStateNotifierを使ったときのデメリットが怖いので,描画のためのクロックとして最初に書いたようにStreamProviderを用意,その間隔と独立して更新したい状態があったら(状態を操作するControllerは必ず作るはずなので)Controller内でStreamを持っておく感じがベストかと.