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

一度の操作で綴り画面が固まり続ける — try/finally の置き方で「処理中フラグ」が戻らない罠

FlutterDart状態管理try-finallyUX綴Photo

Codex と Claude のレビューで浮かんだ「処理中フラグと try/finally」の観察を、勉強のためにまとめたものです。

綴Photo のモバイル版で、「写真を『また今度』にする」操作が、ある条件でそれ以降ずっと固まる可能性のあるコードが見つかった。原因は try/finally の置き方ひとつ。引っかかるのは、try の手前に early return が1行紛れただけで finally がすり抜ける、という見落としやすい形だった。

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

  • 「処理中フラグ」で二重実行を防ぐ定番パターン
  • フラグを立てた後・try に入る前に early return すると、finally が走りません
  • 同じ関数内の似た処理は安全だった、という非対称

処理中フラグが解放されないまま残る

コードで起きていたこと

問題のコードは、ざっくりこういう形だった。

dart
Future<void> passCurrent(Photo photo) async {
  if (sessionActionInProgress) return;     // 処理中ならガード
  setState(() => sessionActionInProgress = true);   // フラグを立てる
 
  final passKey = photo.assetId ?? photo.localPath ?? '';
  if (passKey.isEmpty) return;             // ← ここ。try の「外」で return している
 
  try {
    await db.passPhoto(passKey);
    removeCurrentPhoto();
  } finally {
    sessionActionInProgress = false;       // 必ず false に戻す(はずだった)
  }
}
処理中フラグは mixin の画面 State メモリ。DB にも prefs にも無い。入口ガード処理中なら returnまだ false なら通るフラグを trueメモリに書く保存しない空キーで returntry の外関数を抜ける届かないtry / finallyfalse に戻す場所ここへ入らないメモリ(画面 State)フラグが true のまま残る次の綴る・また今度は入口で弾かれる端末 SQLite(passed_photos)この経路では行を書かないtry に入ったときだけ書く画面を閉じる / 再読み込みでメモリは消える。prefs にも無い。

passKey が空になるのは、写真の ID も保存パスも両方空という稀なケースだが、起きると体験としては「画面が固まった」と感じる致命的な方向のバグだ。

なぜそうなりやすいか

true にする行と finally で false にする行の間に、別の早期 return が紛れ込むと起きます。「フラグを立てる → try → finally で戻す」という流れがある状態で、後から「空チェック」を足すと、setState(true) の直後・try の手前に置くのが順序として自然に見えてしまいます。

同じ mixin ファイル。フラグはどちらも画面 State のメモリ。保存しない。また今度フラグを trueメモリに書く空チェックtry の外で returnfinally届かないメモリ true のまま保存しない取り消しフラグを trueメモリに書く空チェックtry の中で returnfinallyfalse に戻すメモリは戻る保存しない

改善の方向

フラグを立てる前に、空チェックを済ませます。

dart
Future<void> passCurrent(Photo photo) async {
  if (sessionActionInProgress) return;
 
  final passKey = photo.assetId ?? photo.localPath ?? '';
  if (passKey.isEmpty) return;             // ← フラグを立てる「前」に移す
 
  setState(() => sessionActionInProgress = true);
  try {
    await db.passPhoto(passKey);
    removeCurrentPhoto();
  } finally {
    sessionActionInProgress = false;
  }
}
空判定はフラグを立てる前。フラグは画面 State のメモリ。保存しない。空チェック立てる前に判定フラグはまだ false空returnフラグは立てていないメモリは false のまま保存しないありフラグを trueメモリに書くtrypassed_photos へ書くfinally で falseメモリを戻す端末 SQLite の passed_photos は下の経路の try だけが書く。

原則は「フラグを立てたら、その後の全ての離脱経路が finally を通るようにします」。立てる前に弾けるチェックは、立てる前に済ませます。

学びポイント

  • フラグを立てた後に増やした条件分岐は、すべて finally を通るか確認します
  • フラグを立てる前に判定できるチェックは、立てる前にやります
  • 同じファイルでも、安全な書き方と危険な書き方は混在しえます。「隣が大丈夫だからここも大丈夫」とは限りません

このテーマの括り

後始末が必要な処理は「いつ後始末スコープに入るか」を意識して書くと、この種の固着が防げます。


おわりに

原因は「return を1行どこに置くか」だけ。passKey が両方空になる経路は動作確認では稀すぎて踏まないし、コードを眺めても流れが自然に見えて気づきにくいです。第三者レビュー(AI)が「ここは finally を通らない」と指摘して初めて表面化する類のバグです。


この記事は2026年5月時点の自分の理解で書いています。