綴Photo の勉強ノートに戻る
コードレビュー学習2026.06.25

保存した直後に「画面が持っている値」を使うと、保存がなかったことになる — 状態管理のスナップショット

状態管理RiverpodFlutter

Codex と Claude のレビューで浮かんだ「状態のスナップショット」まわりの観察を、勉強のためにまとめたものです。

ここで言う「web」は、綴ったものを web でも見られるように開発中のアプリ(紡Book)のことです。

綴Photo モバイルのコードレビューで、複数の観点(データ保全・UX・設計)が同じ 1 箇所を指摘した。「写真を web に送る」操作が、画面上は「送信しました」と出るのに、実際にはサーバへ何も送られていない可能性がある、というもの。原因は派手なバグではなく、状態管理では誰でも一度はハマる「古いスナップショットを使ってしまう」型だった。

この記事で扱う観察ポイント

  • データベースを更新した「直後」に、画面が持っていた更新前のデータを使うと何が起きるか
  • なぜ状態管理ツールを使っていても起きるのか
  • 「画面が持っている値」と「データベースの値」のどちらを信じるべきか

コードで起きていたこと

「web に送る」ボタンを押したときの処理は、ざっくりこういう順番だった(変数名などは構造がわかる範囲に置き換えている)。

dart
// 1. データベースの「この写真を web に送る」フラグを true にする
await db.markSelected(item.photo.id, true);
 
// 2. 送信用のデータを組み立てる
final memory = Memory(
  photo: item.photo,   // ← これは画面が前から持っていた古い写し
  meanings: item.meanings,
);
 
// 3. 送信する
await sync([memory]);
書いた直後。正は端末 SQLite の photos。画面の写しはメモリにあり、保存しない。端末 SQLitephotos.webSelectedtrue今書いた閉じても残るprefs には無い画面のメモリ(今)item.photo の写しfalse押す前の写し保存しない送信はここを見る画面のメモリ(あと)次の描き直しtrue追いつく保存しない送信は終わっている同じ処理の中では真ん中の false が使われる。対象 0 件。

(この「フラグ」は、はい/いいえだけを持つ小さな目印です。電気のスイッチと同じで、true=オン=web に送ります、false=オフ=送りません。)

そして送信側は「フラグが true のものだけ送る」という絞り込みをしていた。

dart
// 送信対象を絞る処理
for (final photo in photos) {
  if (!photo.webSelected) continue;  // false のものは飛ばす
  // ... 送信データに詰める
}

さらに悪いことに、送信処理は「0 件成功」を返します。呼び出し側はそれを成功とみなして「送信しました」と表示します。ユーザーから見ると、押した・成功表示が出た・でも何も起きていない、という一番たちの悪い形になります。

なぜそうなりやすいか

「うっかり」ではなく、構造的に誰でもハマります。

普段は自動更新に助けられているぶん、「書いた直後の値はまだ古い」という瞬間が見えにくいです。だから気づきにくいです。

実際、同じアプリの別の画面(設定からの一括送信)では、送信前にデータベースから読み直していて、この問題は起きていなかった。同じ「送信する」処理が 2 箇所にあって、片方だけ正しかった、というのも一緒に見つかっている。

改善の方向

考え方はひとつ。データベースを書き換えたら、その後に使うデータはデータベースから読み直します(fresh read と呼ばれます)。

dart
await db.markSelected(item.photo.id, true);
 
// 古い写しを使わず、最新をデータベースから取り直す
final memory = await db.fetchMemory(item.photo.id);
 
await sync([memory]);
正は端末 SQLite の photos。読み直した値はメモリへ載り、保存しない。端末 SQLite photos.webSelected = true閉じても残る。どちらの経路もここは同じ。詳細画面の写し画面メモリ false保存しない送信 0 件読み直した値メモリ true(SQLite から)保存しない。設定の一括はこちら送信する

最小の応急処置としては「写しの該当フラグだけ手で true にした新しい写しを作る」方法もあるが、これは「データベースを読まずに、正解を手で組み立て直している」ことになります。本筋は読み直しのほうだ。

さらに一段引くと、この「フラグを立てる → 読み直す → 送る → 結果を書き戻す」という一連の流れを画面の中にベタ書きせず、ひとつの専用の処理(サービス)にまとめてしまうと、2 箇所で食い違うこと自体がなくなります。

追記(2026-07): このあと実際にその形にした。一連の流れは画面から出して同期の専用サービスへ移してあり、画面側に残したのは結果の表示だけになっている。上に書いた「同じ処理が 2 箇所にあって、片方だけ正しい」状態も一緒に消えた。読み直しも、フラグを立てた直後にデータベースから取り直す形で入っている。

学びポイント

  • 同じ処理が 2 箇所にあると、片方だけ正しく書けていて、もう片方がズレていても気づけません。流れごと 1 箇所にまとめると食い違いが消えます。

このテーマの括り

表示が成功に見えるので、テストや手動確認でも見逃しやすいです(写真そのものは消えないので、壊れたと気づけません)。

「保存できた」と「画面の値が最新になった」は別のタイミングだ、という感覚を持っておくと、この種の事故を予防できます。

おわりに

この手の問題は、状態管理ツールに慣れて「値は勝手に最新になる」という安心感が出てきた頃に出やすいです。自動更新への信頼が厚くなるほど、「今この行で書いた値は、まだ画面の写しに乗っていない」という一瞬が視界から外れます。そこを一度でも踏むと、似た落とし穴はまとめて見えるようになります。

この記事は 2026 年 6 月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。