〜月謝袋アプリWeb化で学んだ3つの壁〜
月謝袋アプリは、おけいことごとの教室の先生と生徒をつなぐPWAです。
主な機能は以下の通りです。
- 月謝受け取りのスタンプ記録(生徒がスマホで確認)
- レッスン日程の管理
- Googleカレンダー連携
- 先生専用の管理画面(月謝スタンプ作成・レッスン日管理・カレンダー一覧)
アプリを作り始めた頃はまだFlutterに苦手意識があったので
FlutterFlowでUI設計し、FlutterのDartコードとしてエクスポートしてカスタマイズする形で開発しました。
壁① dart:io が Web で使えない
ネイティブアプリではプラットフォーム判定に dart:io の Platform を使います。
しかし Flutter Web では dart:io が非対応のため、ビルド時にエラーになります。
❌ ネイティブ時のコード(Webでエラー)
import 'dart:io' show Platform;
Platform.isAndroid ? 35.0 : 27.0
✅ Web対応コード
import 'package:flutter/foundation.dart' show kIsWeb;
kIsWeb ? 27.0 : 35.0
dart:io を使っているファイルを全て探して kIsWeb に置き換える作業が必要でした。FlutterFlowが自動生成したコードの中にも潜んでいたので、地道な作業になりました。
壁② StreamBuilder の多用でFirestoreがクラッシュ
FlutterFlowが生成するコードには StreamBuilder(.snapshots())が大量に含まれています。
ネイティブアプリでは問題ないのですが、Web版では同時に張れるFirestoreのリスナー数に制限があります。

今回のホーム画面では 3年×12ヶ月=36マス に加え、各セルに AuthUserStreamWidget が使われていました。
計算すると……
StreamBuilder × 36 + AuthUserStreamWidget × 12 = 48本以上の常時接続 → FIRESTORE INTERNAL ASSERTION FAILED 💥
解決策:StreamBuilder → FutureBuilder に全置換
- StreamBuilder(.snapshots())→ FutureBuilder(.get())に変更
- AuthUserStreamWidget はページ全体で1個だけ使用
- データ更新後は setState() で手動再取得
リアルタイム更新が不要な箇所は FutureBuilder で十分です。ネイティブではStreamを使いまくっても問題ないのに、Webでは大きな違いになることを学びました。
壁③ フィールド名の不一致でデータが表示されない
ネイティブアプリでは FlutterFlow が生成した createPostRecordData() を使ってFirestoreに保存していたため、
フィールド名は isPaid で統一されていました。
しかしWeb版を独自実装した際、うっかり Map で直接保存してしまい is_stamp_true というフィールド名で保存されてしまいました。
❌ 誤った実装(フィールド名が違う)
await PostRecord.collection.add({
'is_stamp_true': true, // ← FlutterFlowのisPaidと別物!
...
});
.0 : 27.0
✅ 正しい実装
await PostRecord.collection.doc().set(
createPostRecordData(
isPaid: true, // ← FlutterFlowの正式フィールド
...
),
);
さらにフォーマットの不一致
さらに month_dropdown のフォーマットも ネイティブ版は ‘month_dropdown: ’01” (ゼロパディングあり)なのに対し、Web版は ‘1’ で保存していました。
こうした細かい不一致が積み重なって「データはあるのに表示されない」という症状になっていました。
Chrome DevToolsのコンソールにデバッグprintを仕込んで、ひとつひとつ原因を特定していきました。
まとめ:FlutterネイティブとWebは「別物」
今回の移植作業で痛感した教訓を3つにまとめます。
- dart:io は Web非対応。kIsWeb(package:flutter/foundation.dart)に統一する
- StreamBuilderはWebでは多用禁止。FutureBuilder + setState() に切り替える
- データ保存は必ずFlutterFlowの生成メソッド(createXxxRecordData)を使う。
独自Mapで保存するとフィールド名がズレる
私の場合は
FlutterFlow→Flutterネイティブ→FlutterWeb
と、特にかなりややこしい流れになってしまいました
「Flutter はマルチプラットフォーム対応」とはいっても、内部の仕組みはネイティブとWebで大きく異なります。
最初からWeb対応を念頭に置いて設計するか、移植時にはこれらの点を意識してチェックすることをおすすめします。
同じところで詰まっている方の参考になれば嬉しいです。
