一覧画面がスケルトン表示のまま、いつまでも進まない。RTDB のルールもネットワークも App Check も疑いましたが、どれも違いました。原因は Flutter 側の書き方、それも多くの入門記事どおりに書いたコードでした。
01 — やってしまった書き方
stream: にサービス呼び出しを直接書く
StreamBuilder(
stream: DiscoverService.instance.watchOnline(), // ← これ
builder: (context, snapshot) { ... },
)
一見なんの問題もありません。実際、画面がリビルドされなければ正しく動きます。
問題は build() が呼ばれるたびに watchOnline() が実行され、
そのたびに新しい Stream(=新しい RTDB のリスナー)が作られることです。
02 — なぜデータが消えるのか
リビルドの速さがデータ到着を追い越す
この画面は initState() でブロックリストとトークルームも購読していました。
そのどちらかが一度でも発火すると setState() が走り、build() が再実行されます。
すると StreamBuilder.didUpdateWidget が古いリスナーを破棄し、新しいリスナーに差し替えます。
このとき AsyncSnapshot は connectionState だけを waiting に戻します。
画面側が「connectionState == waiting ならスケルトンを出す」と書いていると、
せっかく届いたデータがスケルトンの判定に飲み込まれて捨てられます。
RTDB への実際の往復よりリビルドのほうが速い、あるいは両者が競合すると、
最初のイベントが永久に「無かったこと」にされ続けます。
03 — 修正
Stream は initState で一度だけ作る
late final Stream<List<User>> _onlineStream;
@override
void initState() {
super.initState();
_onlineStream = DiscoverService.instance.watchOnline();
}
// build() では作らずに参照するだけ
StreamBuilder(stream: _onlineStream, builder: ...)
同じアンチパターンがチャット画面(メッセージ送信のたびに setState が走る)、
トーク一覧、ブロックリストにもありました。合計4ファイルを同じ方針で直しています。
トーク一覧とブロックリストは StatelessWidget でしたが、
initState() を使うために StatefulWidget に変えました。
「自分では setState していないから安全」とは限らず、親経由でリビルドされます。
04 — 覚えておくこと
「画面を離れたら cancel」だけでは足りない
リスナー管理の注意点としてよく言われるのは「画面を離れるときに必ず cancel() する」ですが、
それだけでは今回の症状は防げません。もう一つ、
同じ画面が生きているあいだは Stream を作り直さないという原則が要ります。
リアルタイムデータベースを扱う画面を追加するときは、この2つをセットで確認しています。
この不具合はとりまトークの Web 版で見つけたものです。
Follow
こういう記録を置いています
個人開発でつまずいた話や、作りかけのものは Instagram(@kenpiro1012)と note にも置いています。 記事の一覧は記事ページにまとまっています。