モバイル / デスクトップ
綴Photo の勉強ノート
写真に言葉を綴じ、本に綴じるまで。
作る過程で出会った技術と気づきを、自分の勉強のために残しています。
全96本
シートを閉じる方法は4つあって、自分が頼んだのは1つだけだった
本文の上限を5000字に上げた2日後、その長文を守るものが1つも無いことに気づいた。閉じる経路を数えたら4つあって、3つは頼んだ覚えがなかった。フレームワークが用意してくれている入口の数え方の話。
上限を上げるコストはゼロだと数えた。器が伸びる面だけ数え忘れていた
本文の文字数上限を 300 から 5000 に上げるとき、影響が出る場所を4つ数えて全部ゼロだと確認した。その日の夜、試しに長文を1本綴じたら文字が左端で切れた。数えたリストに「器が伸びる」が入っていなかった。
「直す前に赤にする」を守ったら、同じ日に2回、赤にできなかった
バグを直したのにテストが赤にならない。追ったら1回目は「直そうとしたバグが実在しなかった」、2回目は「テストデータの並びが悪くて旧実装と差が出なかった」だった。赤にならない理由は実装側と検証物側の両方にある。
テストは通っていた。写真を1枚も入れていなかったから — アプリ更新時のデータ移行(マイグレーション)
古いバージョンのデータベースから最新へ上げるテストは通っていた。ただ中身が空で、実際にデータを載せて流し直したら、日時が西暦55840年になる段と、データベースが二度と開かなくなる段が出てきた。どちらもそのファイルの先頭に「こうするな」と書いてあった。
1箇所に閉じ込めて検査まで置いたのに、裏口がもう1つ開いていた — Flutter の投げっぱなし Future
誰も待っていない非同期処理の中で例外が出ると、アプリ自身ではなくフレームワークが内容をコンソールへ吐く。自分のコードから debugPrint を探す検査には原理的に映らない。前に塞いだ穴と同じ本文が、別の出口から出ていた。
深夜に綴った言葉が前の日として表示される — USE_TZ と isoformat() のずれ
Django で USE_TZ=True にしていると、モデルの datetime は UTC のまま持たれる。TIME_ZONE の設定はそれを自動では変えない。生の isoformat() を返す API と DRF のシリアライザを混ぜると、同じデータが経路によって別の日付になる。
「やめる」だけ元に戻していたので、「もう一度やる」が嘘をついた
公開を取り消す処理は、失敗したらローカルの状態を元へ戻していた。公開する処理は戻していなかった。そのせいで再公開だけが「公開中」と表示されたまま失敗し、あとから走る自動処理が「web で外れたので下ろしました」と、誰も外していないのに web のせいにしていた。
名前を信じて、日記の本文を端末のログに流していた — debugPrint はリリース版でも喋る
Flutter の debugPrint は名前に反してリリースビルドでも出力される。保存が失敗したときの例外をそのまま出していたので、綴った本文と写真の絶対パスが端末のログへ流れていた。直すついでに「囲み忘れを検査する」のをやめて、呼べる場所を1つに絞った話。
束ねて寝かせたら、片方は二度と起きなかった — 永久に鳴らないトリガーに縛っていた話
AIレビューが「新規の P1」として出してきた指摘が、1ヶ月前に自分で「あとでやる」に畳んだ項目だった。無関係な2つを1つのブロックに束ねた結果、片方は永久に鳴らないトリガーに縛られ、誰にも見えない場所で1ヶ月寝ていた。ついでに、その被害の見積もり自体を間違えて5箇所にコピーしていた話も。
中身の無い返事が「全部消せ」という指示になっていた
公開中の写真一覧を取りに行く処理が、本文が空の応答を「公開ゼロ」として受け取っていた。空リストは突き合わせにとって破壊的な指示で、続く送信がサーバ側の公開を全件削除する。同じファイルの説明文には、その方針と逆のことが書いてあった。
回数を増やしても直らないなら、待ち方の問題ではない — テストの中で永遠に完了しない Future
同期処理に順番待ちを1つ入れたら、無関係な widget test が3本落ちた。待ち足りないのだと思って pumpAndSettle を増やしていったが、5回にしても直らない。原因はフィールド初期化子に置いた Future.value() が「別のゾーン」で作られていたこと。
画面で約束していることを、支えていたのは他人のライブラリの実装詳細だった
「位置情報やカメラの情報は取り除いてから送ります」と画面に書いてある。デスクトップ側はちゃんと剥がしていたが、スマホ側は剥がす処理を通っていなかった。それでも今のところ漏れていないのは、写真を取ってくるライブラリがたまたま作り直した画像を返すから。
いちばん強く書いていたルールが、唯一チェックされていなくて、すでに破られていた
アーキテクチャのルールは5本あって、4本は自動チェックが付いていた。付いていない1本が、AI への指示書でいちばん強い言い方をしているものだった。チェックを書いて走らせたら、知らなかった違反が2件出た。
5日前に書いた「これが落ちたら合図」が、ちゃんと鳴った
症状を抑える対処を入れたとき、同じ場所に「根っこが直ったらこのテストが落ちる。その失敗はフラグを見直してよい合図」と書いておいた。5日後に根っこを直したら実際に落ちて、赤が指示書として機能した。
判定の軸が食い違っていたので揃えようとしたら、揃えるほうが間違いだった — Flutter の幅判定と OS 判定
同じ画面の中で、片方は「画面の幅」で、もう片方は「OS の種類」でモバイル / デスクトップを判定していた。指摘を受けて揃えにいったが、調べたら2つは別の問いに答えていた。揃えずに、違う理由を書いた。
何も送っていないのに「全部うまくいった」— 成功判定を1本にまとめる途中で手が止まった話
4箇所に散らばった同期の成功判定を1つの語彙にまとめようとしたら、ちょうどいい判定が既にあった。乗り換えようとして止まった理由は、every が空のリストで true を返すこと。過去に一度踏んだ穴を、リファクタ(見た目も挙動も変えず中身の作りだけ直す作業)で開け直すところだった記録。
受け取れる形なのにわざと弾く — 部分リストを黙って受けるとデータが静かに消える
サーバから「いま公開中の写真」の一覧を受け取る処理を書いた。この一覧は「ここに無いものは消せ」という指示として読まれるので、将来ページ分割された形も受けられるようにする3行を足すと、半分だけ届いた日に残り半分を消す指示になる。足さずに、形が違ったら落ちる方を選んだ。
分けたはずの作業が分かれていなかった — スライスの独立性は「実装できるか」では測れない
同期まわりの改修を2つのスライスに割って、判断が要らない方から着手しようとした。実装直前に洗い直したら、片方だけ入れるとその日の未明に検証で合格した挙動が壊れることが分かった。しかもその壊れ方は、設計書の冒頭に自分たちで書いてあった。
浅く傾けた写真のフチほどギザギザが目立つ — Flutter の Transform.rotate とアンチエイリアス
紙のアルバムっぽさを出すために写真を約0.8°だけ傾けたら、実機でフチがギザギザになった。浅い角度ほど段差が横に長く伸びて目立つ。Transform.rotate に filterQuality を渡して、レイヤー経由で回す経路に切り替えた記録。
DB の型を UI から隠したら、新しい抜け穴ができた — 列の写し忘れをテストで塞ぐ
DB の型を UI から隠したら、列を足したときに写し忘れる経路が新しくできた。コンストラクタの required と、全列を1対1で突き合わせるテストの二重で塞いだ話。
「また今度」に戻せない時がある — 巻き戻し先を覚えるフラグが、途中の付け替えで消えていた
挙動を変えないはずのリファクタを実機で確かめていたら、綴る画面が詰んだ。原因の既存バグを直した『案A』自体に、mood を付け替えると復元フラグが消える次の層のバグが潜んでいた、という入れ子のデバッグ記録。
既存の違反は責めず、新しい違反だけ止める — アーキテクチャ検査を一方向ラチェットにする
綴Photo に「層の依存方向」を検査するテストを入れた。既存の違反が34個ある状態で、全部直さずに悪化だけ止める。allowlist と stale 検出で一方向ラチェットにした話。
同期のコアを触らずに『何が公開されるか』を見せる — ゲート関数という置き場所
綴Photo で「クラウドに何が出るか」を初回だけ確認させる機能を、同期の順序を握るコアには一切触らず UI 層のゲート関数1個で足した話。テストも軽い形で書けた。
モデルを書き換える前に、表示を変えてみる — ゲージを『進捗』から『現在地』に
綴Photo で「決定済み/全体」の進捗ゲージが世界観と合わなくなった。集合モデルを書き換えず、表示の意味付けだけ変えて解決した5分の話。
写真1枚に何を残したか — 状態を「集合」で持って union で数える
綴Photo の綴る画面に mood ボタンを分離して置いた時、cursor の状態機械を「排他」だけから「排他+共存」に組み直した話。3集合で持って frontier は union で数える。
別アカウントで入るとき、ローカルをリセットすると綴りがサーバで二重に立つ — 対策の副作用まで踏んでから決める
「別アカウントで入り直したら、端末のローカルDBの同期マーカーだけリセットして再同期すればいい」という初手の対策が、backend 側で綴りを二重に立てる副作用を持っていた。だから「そもそも別アカウントで入らせない」に切り替えた話。
CI が緑でも本番のデータ移行は壊れうる — スキーマの写しが古いまま、という死角
テストは緑、でもデータベースのスキーマの写し(snapshot)が古い版で止まっていた。手書きのマイグレーションテストが通ることと、本番ユーザーの DB が無事に移行できることは別、という観察。
「再エンコードすれば位置情報は落ちる」はライブラリ次第で成り立たない — 思い込みをテストで縛る
アップロード用に写真をリサイズ・再エンコードすれば EXIF(撮影機材や位置情報)は剥がれる、と設計メモに『確認済』と書いてあった。剥がれていることを縛るテストを足したら、剥がれていなかった、という観察。
「今は届かない穴」を重要度ゼロにしない — レビューの優先度は「いつ効くか」で決まる
機能フラグの裏で今は到達できない問題を「深刻さ」だけで並べると忘れてしまう。「いつ効くか」をもう一軸にして、見落としを忘れるのでなく待たせる。
ログアウトで端末のデータを消す、をやめた — 消す道を一本増やすほうが怖い
共有端末対策としてログアウト時にローカルDBを消す、は一見正しい。でも思い出アプリでは「消す経路を増やす」こと自体が一番の事故になりうる。なぜ消さないと決めたかの記録。
保存した直後に「画面が持っている値」を使うと、保存がなかったことになる — 状態管理のスナップショット
データベースを更新した直後に、画面が握っていた古いデータで処理を続けると、その更新が無視される。綴Photo の「web に送る」が成功表示のまま 0 件送信になっていた話。
モバイルに本物のログインを足す④ — 動いてはいた。でもアプリは自分がログイン済みだと知らなかった
前回ブラウザ経由でログインを通したが、設定を見ても『ログイン中』かどうか出ない。読んだら復元処理は書いてあるのにどこからも呼ばれていなかった。その一行を画面に出すまでに要った配線と、失効トークンが実機で勝手に蘇った話。
モバイルに本物のログインを足す③ — 中継をレビューに掛けたら、PKCE が守る線と『今は直さない』が見えた
前回アプリ側に作った『引換券で token を受け取る』仕組みの、web 側の中継ができた。まるごと Claude にセキュリティレビューしてもらって学んだのは2つ。PKCE が守ってくれる範囲には正確な境界があること。そして指摘を全部直すのが正解じゃなく『今直す/今は直さない』を決めるのが自分の仕事だということ。
ログインを起動の壁にしない — ゲートは同期の側だけに置いた
「ログインを足す」と聞くと起動時のログイン画面を思い浮かべるが、ローカルだけで全機能が成立するアプリでそれをやると、自分の強みを入口で潰すことになる。壁を立てず、ゲートを同期の側だけに置いた判断の記録。
アプリのログイン、結局どういう形? — 『誰が認証するか』と『どう戻すか』を分けたら、ほどけた
自分のアプリのログインがどういう形なのか毎回こんがらがるので整理した。鍵は『誰がログインさせるか(web に丸投げ)』と『結果をどうアプリに戻すか(戻り道)』を分けること。戻り道は横取りされない App Links(OS 検証付きの https リンク)=“検証済みの郵便受け”にした、という判断の記録。
写真が潰れていたのは表示じゃなくデコードだった — Flutter の画像サイズ指定と、写真メインアプリの cover/contain 判断
デスクトップで一番大きく出す写真が縦に潰れていた。原因は表示じゃなくデコードで、幅・高さ両方を枠サイズで渡すと比率が壊れるという Flutter の落とし穴だった。直すついでに『写真メインなら全画面 cover より全体 contain』を選んだ話。
モバイルに本物のログインを足す② — Google を持たせず web に丸投げし、トークンは『引換券』で受け取る
モバイルに本物のログインを載せる土台。アプリが Google と直接やり取りする方式をやめ、ブラウザで自分の web ログインを開いて token を受け取る形に倒した。戻り道は漏れる前提で、token そのものではなく使い捨ての引換券+PKCE で受け渡す。最後にレビューが拾った『ログインボタンが二度と押せなくなる罠』を塞いだ記録。
モバイルに本物のログインを足す① — 既存の同期を1行も変えず、トークンを『毎回読む』継ぎ目にする
モバイルに本物ログインを足す土台を組んだ。既に動いている同期・アップロードのコードを一切変えずに済ませるため、認証の窓口を1個の契約(AuthSession)に切り出して差し込んだ。トークンを焼き込むのをやめて毎回読む形にし、401 で一回だけ自動で取り直す・同時の取り直しは1回にまとめる(単一フライト)まで。最後にレビューが拾った『一番大事なトークンだけ無防備』も塞いだ記録。
選んだ写真だけ web に上げる同期にした — 「選んだ意図」と「実際に届いたか」を分けて持つ
モバイルで綴ったものを web に出す同期で、「全部送る」をやめて「ユーザーが選んだ写真だけ送る」に変えた。選んだ意図(webSelected)と、実際にサーバへ届いたか(syncState/remoteUrl)を別々に持ち、画面の表示はそこから計算で出す。curated にした理由と、状態を2層に分けた設計の記録。
写真だけ残って、綴りが消える — それを防ぐのが Drift のトランザクションだった
綴Photo モバイル版で写真を1枚保存すると、photos と meanings(綴り) の2回 INSERT が走る。これを別々にやると、片方成功・片方失敗で『写真だけ残って綴りがない』孤立データができる。トランザクションで原子性を担保する、を腹落ちさせた学習メモ。
画面を幅で出し分けてたら導線が消えた — 分岐軸が2本ねじれていた話
綴Photo のデスクトップ版で、ウィンドウを狭めると『フォルダから綴る』導線が消えるバグ。原因は、画面を出し分ける軸(画面幅)と、写真の取得元を決める軸(OS)が別々で、特定の幅帯でねじれていたこと。2つの分岐軸を揃えた学習メモ。
サムネのサイズを渡さないと、毎回フル画像を読み込んでいた — Flutter photo_manager の thumbnailLogicalSize と decodeLogicalSize
綴Photo モバイル版で写真表示が重かった原因。AppPhotoImage の thumbnailLogicalSize というサイズヒントが有るか無いかで、画像の取得経路がまるごと変わる。無いとOSのサムネキャッシュを使えず、フル解像度を自前デコードしていた。似た名前の decodeLogicalSize と混同していた、を解いた学習メモ。
アクセシビリティ対応を『マウスで説明が出るやつ』だと思っていた — 読み上げ機と、どこまでやるかの線引き
綴Photo モバイル版で、AIのコードレビューに『写真や入力欄にアクセシビリティ情報が足りない』と指摘された。直す前に、そもそも自分がそれをツールチップやデスクトップの話と勘違いしていたことに気づく。スクリーンリーダー向けの目に見えない情報だと分かった上で、視覚前提のアプリでどこまでやるか線を引いた記録。
識別の列を1本から2本に分けたら、5バージョン前のマイグレーションが静かに壊れていた
「また今度」した写真の識別を1列から2列に分ける作業で、列の UNIQUE 制約を外したら、5バージョン前の DB マイグレーションが壊れていた。テストは全部緑のまま。実装後に別の目(多視点の敵対的レビュー)に通して初めて見つかった、制約の変更が遠くで壊れるという記録。
Django を『材料・料理・配膳』に分けたら、view が読めるようになった
綴Photo のバックエンドで selector / service / view を分けたときのメモ。DB から取る・ルールで選ぶ・受け答えする、を別々の係にすると view が薄くなって、後から読み返しやすくなった。
ソーシャルログインは入口が増えるだけ — 出口は変えなかった話
綴Photo に Google と Apple のログインを足したときの設計メモ。本人確認の入口は増えるけど、トークン発行と Cookie の出口は既存に寄せた判断と、Google と Apple の非対称さでつまずいた所。
見えない ≠ 無い — dart-define で焼き込んだトークンと、kDebugMode で隠したボタン
綴Photo モバイル版のセキュリティ寄りの指摘。開発用の認証トークンを --dart-define で渡していたが、それはビルド時にバイナリへ文字列として焼き込まれる=『環境変数だから隠せる』が成り立たない。同期ボタンを kDebugMode で隠していたのも『見えないだけで送信コードは生きている』状態だった。どちらも『見えない』を『無い・安全』と読み替えていた話。
失敗を『成功』に化けさせる穴埋め — null と空文字を取り違えると、やり直しの機会ごと消える
綴Photo モバイル版の画像アップロードで見つかった一番重い穴。サーバ応答からURLを取れなかったとき `?? ''` で空文字を入れていたせいで、失敗が『アップロード済み』として固定され、二度と再アップロードされなくなっていた。null(まだ無い)と空文字(無いと確定)を同じものとして扱うと、リトライの土台そのものが壊れるという話。
「静かな天井」を直したら、別の「静かな天井」を作っていた — 写真ピッカーの打ち切りと、2周目のレビュー
綴Photo モバイル版の写真ピッカーが、カメラロールの先頭300枚で黙って打ち切っていた。エラーでも空白でもなく、ただ途中で終わる『静かな天井』。ページングに直したら、その修正が今度は『追加ロード失敗時にスピナーが回り続ける』別の静かな天井を作っていて、わざと穴を探す2周目のレビューが捕まえた話。
サムネは見えるのに、タップしても何も起きない — iCloud の『本体はクラウド』と、取得して捨てる前提条件
綴Photo モバイル版の写真ピッカーで、サムネは見えて選べるのにタップしても保存シートが開かない、という無言の行き止まりが iCloud ユーザーに起き得た。原因はタップ処理が『写真の実体ファイルを取れること』を前提条件にしていたこと。しかもその実体ファイルは後続で使っていなかった。表示に要る重さと、開発者が無意識に前提にした重さのズレの話。
レビューの5指摘を上から順に直さなかった — 実害で並べ替えて、判断が要るものは残した
Codex のフルレビュー5件を別のAIで再検証したあと、全部つぶさずに『今この瞬間の実害』と『自分の判断が要るか』で並べ替えて、今直すものと、判断を残すものに仕分けた記録。あわせて、テストが回せない日に差分を読んで捕まえた、Django のバージョン差で消えたAPIの話。
コードは正しかった。古かったのは走ってるサーバ
スマホの写真をサーバへ上げる機能が、なぜか毎回「別の写真」に画像を貼ってしまう。コードを直す前に本当にコードが悪いのかを確かめたら、犯人は『走ってる開発サーバが古いまま』だった、という半日溶かしたデバッグの記録。
写真の画像を、撮った端末から取り出して届ける — バイトの在り処がOSで違う話
スマホで綴ったデータをサーバへ届ける連載の続き。これまでテキストだけが届いていたところに、写真の画像本体を流す回。画像のバイトが端末のどこにあるかはOSで違っていて、そこを吸収しながらアップロードを組んだときの判断の記録。
同期を冪等にする② — サーバのIDを端末に書き戻して片肺を閉じる
スマホで綴ったデータをサーバへ同期する仕組みで、前に「片肺だ」と書いた保険を片方閉じた。サーバが返したIDを端末に書き戻す処理を入れたときの、2つの判断(送信は変えず記録だけにする/新しい置き場所を作らずコード生成を避ける)の記録。
テンプレを磨く前に「管を通す」を選んだ — 3つの器と2本の管
綴Photo は mobile・backend・web の3つでできている。見栄えのするテンプレを増やすより先に、スマホで綴ったものが web で見える「管」を1本通すことを選んだ。その判断と、通して初めて見えた全体の構造(3つの器・2本の管・両端の変換)、そして両端のAPIの叩き方をまとめる。
同期を冪等にする① — 綴り1つにIDを2本持たせて「二重」を防ぐ
綴Photo で、スマホで綴ったデータをサーバに同期する仕組みを作っている。1つの綴りに、端末が作るUUIDとサーバが採番するIDの2本を持たせた。なぜ2本いるのか、それがどう「二重に登録されない(冪等)」保険になるのか、そしてこの記事を書いた時点ではその保険が片肺だった、という記録。
アプリの綴りを backend に送る『出口』を、内側から積んで固めた — 変換・HTTP・サービスの4層と WSL の罠
綴Photo のモバイルアプリが綴ったデータを、ローカルの backend に送る『同期の出口』を作った。モバイルと backend で名前や形が違う契約(body→text、撮影日時の転写、favorite の不在)を1枚の純関数に閉じ込め、変換→解析→HTTP→サービスと内側から積んでテストで固めてから、最後に実機の HTTP で backend につないだ。途中、Windows から WSL へコマンドが空で渡る罠も踏んだ。
AI レビューの指摘 20 件中、無修正で生き残ったのは 6 件だった — 反証検証という二段構え
綴Photo のコードベースを Claude の多段レビューにかけたら medium 以上の指摘が 20 件出た。それを『反証専任』のエージェントに全件渡して実コードで潰させたら、確定したのは 6 件だけだった。偽陽性が消えていく過程と、『これは仕様』コメントが指摘を未然に防いだ実例をまとめる。
Drift のマイグレーションは「凍らない」— addColumn / alterTable が見るのは最新のテーブル定義
綴Photo モバイル版のレビューで見えた Drift(や同種の ORM)固有の罠。マイグレーションの addColumn / alterTable が参照するのは『書いた当時のスキーマ』ではなく『今この瞬間の最新テーブル定義』で、後から列を足すと過去の段が duplicate column で落ちうる。なぜ凍らないのか、どう守るかをまとめる。
マイグレーションが一度もテストされていなかった — 旧バージョンの DB を作って『通す』テストの書き方
綴Photo モバイル版のレビューで見つかった、DB マイグレーションの落とし穴。全 DB テストが新規作成(onCreate)だったため、約170行のアップグレード処理が一度もテストで走っていなかった。旧バージョンの DB を実ファイル上に作って、マイグレーションを実際に『通す』テストの組み方をまとめる。
ファイル選択の許可は、アプリを閉じたら消える — デスクトップ OS のサンドボックスと永続ファイルアクセス
綴Photo のデスクトップ版コードベースをレビューしていて浮かんだ、サンドボックスと永続ファイルアクセスの観察。ファイルピッカーで得た許可は原則『その場限り』で、再起動後も同じパスを読み続けるには OS ごとに別の作法が要る。Windows は宣言で対処済み。macOS は執筆時点で未解決だったが、その後の実機検証で問題の実在を確認し、security-scoped bookmark の実装で解消した(末尾に追記あり)。
外部キーを張っても、索引は付いてこない — SQLite と FK 列のインデックス
綴Photo モバイル版のレビューで、外部キーの列にインデックスが張られていなかった。主キーや UNIQUE と違い、SQLite は外部キーの列に自動でインデックスを張らない。watch / join が全件走査になる前に塞いだ記録。
宣言しただけでは守られない — SQLite の外部キーは PRAGMA で有効化しないと強制されない
綴Photo モバイル版のレビューで、ORM(Drift)で宣言した外部キーが SQLite では強制されていなかった。接続ごとに PRAGMA foreign_keys = ON を実行しないと、REFERENCES はただのドキュメントになる、という見落としの記録。
非同期にしたのに画面が固まる — Dart のアイソレートと、待ち時間をどこに置くか
綴Photo デスクトップ版で 5000〜6000 枚の写真フォルダをスキャンすると、ローディングのアニメーションごと UI が固まった。原因は『async と書いてあるのにメインアイソレートで CPU 作業をしていた』こと。Isolate.run でバックグラウンドに退避した話と、ついでに『解像度の遅延読み込みを却下した理由』という UX 設計の判断もまとめる。
「13月」を指定したら黙って翌年1月の写真が出た — Dart DateTime の暗黙正規化と入力チェック
綴Photo の画面遷移(ルート)から月を復元する経路で、month=13 のような不正値が検証されていなかった。Dart の DateTime は範囲外の月を例外を投げずに黙って正規化する(13月→翌年1月)ので、クラッシュせず別の月を表示してしまう。アプリ自身は生成しない値だが、似た別経路は検証していたのに片方だけ素通りだった、という非対称の話。
デスクトップで『共有』が出るのは違う — 同じ『書き出す』でもプラットフォームで導線を変える
綴Photo のデータエクスポートが、デスクトップでも share_plus の共有シート(メール/アプリ宛て)を開いていて、ファイルを保存する導線が無かった。モバイルは共有シート、デスクトップは『名前を付けて保存』ダイアログ、と出し分けた話。ついでに JSON に絶対パスを残さないようにしたプライバシーの直しも。
1件の失敗でフォルダ走査を止めない — symlink ループと『途中で投げる例外』の罠
綴Photo デスクトップ版の再帰フォルダ走査を堅牢化した記録。dir.list(recursive: true) のデフォルトは symlink を辿るので、ループするリンクがあるとスキャンが永遠に終わらない。さらに、読めないサブフォルダや消えたファイルの例外が走査全体を中断させていた。followLinks: false / handleError / ファイル単位 try-catch で『1件の失敗で全体を止めない』走査にした話。
新規インストールのユーザーだけ DB の制約が弱い — onCreate と migration の非対称
綴Photo モバイル版のレビューで見つかった、ローカル DB(Drift / SQLite)の罠。重複防止の UNIQUE インデックスをマイグレーションでしか作っておらず、新規インストールのユーザーにはそのインデックスが存在しない。onCreate と onUpgrade を二重にメンテする難しさを題材にまとめる。
『端末で完結』を謳うアプリの隠れたネットワーク依存 — Google Fonts のランタイム取得
綴Photo モバイル版のレビューで見つかった、オフライン完結を価値にするアプリが起動時に Google のサーバーからフォントを取得していた話。google_fonts パッケージの allowRuntimeFetching の挙動と、フォントを bundle する対策をまとめる。
『この指摘、本当に危険?』は feature flag と throttle と設計意図で変わる — AI レビューの読み方
綴Photo の Web / モバイル両方のレビューを通じて見えた、AI が挙げた指摘の『重要度』とその実害が一致しないケースの整理。feature flag で本番では無効、レートリミットで既に緩和、そもそも設計意図——コードの字面だけで危険度を決めないための読み方をまとめる。
一度の操作で綴り画面が固まり続ける — try/finally の置き方で「処理中フラグ」が戻らない罠
綴Photo モバイル版のレビューで見つかった、try/finally の置き方ひとつで UI が固まる罠。処理中フラグを立てた後、try に入る前に early return すると finally が走らずフラグが立ったまま残る。同じ関数の中の似た処理は安全だった、という非対称も含めてまとめる。
コードレビューで誤認されないコメントの書き方 — AI が自分のコードを『バグ』と誤認したケース
綴Photo モバイル版の修正中、AI が既存のコードを『バグ』と誤認した。原因は『コメントに何が書かれてないか』だった。呼び出し文脈・誤認されやすい仕様・経緯の3点ルールで直した記録
月ごと12回スキャン → 年1回に集約した話 — 性能改善の典型パターン
綴Photo モバイル版で「写真なし月を隠す」ボタンが遅かった。OS への重い問い合わせを12回繰り返していたのを、1回に集約した記録。『重い処理の回数を減らす / 軽い処理にまとめる』という性能改善の型の話
AI が書くコードは「ファイルは普通にある」前提だった — デスクトップ対応の異常系3つ
綴Photo モバイル版にデスクトップ対応を足したとき、モバイルには無かった『ファイルが思い通りにない』ケースが3つ噴き出した記録。サンドボックスでファイルが読めない / ファイルが移動されて見つからない / 画像のデコードに失敗する。完成したつもりでも Codex のコードレビューを走らせると、デスクトップでファイルを直接触るがゆえの OS 固有の設定やファイルシステムの現実に根ざした穴が出てくる、という話
コードレビューの指摘を、あえて途中で止める — マジックストリング対策の3段階
綴Photo モバイル版のコードレビューで「マジックストリング」を指摘された。文字列 → 定数 → enum と段階的に直せるが、どこまでやるかは別の判断。途中で止めた理由と、その判断に至るまでに AI と一緒に概念を整理した記録
「写真なし月を隠す」が未来月まで隠せる罠 — 境界条件の見落とし
綴Photo モバイル版で、期間選択画面の「写真なし月を隠す」機能が、今日が 5月14日のとき 6〜12月まで「写真なし=完了」扱いで隠せてしまう罠を直した記録。1〜12月を全部スキャンしていたのが原因で、走査範囲を「現在月まで」に制限する境界条件の話
写真権限を「限定」に変えただけで、綴った記憶の写真が消えたように見える罠 — Flutter photo_manager / AssetEntity.fromId() の null
写真権限を「限定アクセス」に変えると、綴った記憶の写真リンクが連鎖的に壊れる罠の記録。落とし穴は、photo_manager の AssetEntity.fromId() が null を返すのは「削除」とは限らない点。対策はフルアクセスのときだけメンテ処理を走らせること
「ベストプラクティス全部入れ」をあえてやらない — DRF throttle のキャッシュを Redis にせず据え置いた理由
アクセス回数の制限カウンタが「サーバーを増やすとバラける / 再起動で消える」と指摘された。Redis を入れるかどうかを、現実のスケール・固定費・外側(Cloudflare)に寄せる方針の 3 軸で評価して、プロセス内メモリのまま据え置く判断をした記録。
ログイン方法を1つ消すのに、6レイヤー触ることになった — email/password を閉じて OAuth only にした作業ログ
email/password ログインを閉じるだけのつもりが、認証は settings・URL・view・serializer・tests・frontend の6レイヤーに根を張っていた。1機能を綺麗に消すには足あとを全部追う必要がある、という作業の記録。閉じる動機(攻撃面の縮小)と、allauth の W001 を silence する理由付けもあわせて整理する。
画像アップロード上限を考え直した話 — 1ファイル / 合計 / ピクセル数 / decompression bomb
画像アップロード API の上限が「1ファイル 20MB × 最大 50 件」だけだった。1リクエストで理論上 1GB 流せる状態で、認証必須でも誤投入や悪用で CPU/メモリが死ぬリスクが残っていた。1ファイルサイズ / 合計サイズ / ファイル数 / ピクセル数の 4 軸で上限を再設計した記録と、decompression bomb 攻撃への二重防御の話。
ログインを何度でも試せる状態だった — Django REST Framework のレート制限(Throttling)
ログインなどの認証窓口に試行回数の上限を入れておらず、パスワードを総当たりで試せる状態だった。DRF Throttling と django-ratelimit でどう塞いだか、使い分けも含めた記録。
「また2箇所直してる」と気づいて、Flutter のファイル構成と mixin で整理した話
綴Photo の Desktop 対応で散らかったコードを、ファイル構成のリファクタと mixin の切り出しで整理した記録。mixin の `with` / `on` / 型パラメータ `T` まで踏み込んで書いた
「保存しました」が見にくかったので、言葉から作り直した — 綴Photoのフィードバック表示
保存トーストが見にくくて微妙だったので作り直した。最初は画像も考えたけど、言葉とアニメに落ち着いた、綴Photoのフィードバック表示の話。
コードで「手作り感」を出す3つのテクニック — Flutterの微妙な傾き・テープ・付箋
`Transform.rotate(angle: 0.014)` という小さい数字が、写真アプリの印象を全部変えた話
保存が途中で失敗すると半端なデータが残る — transaction.atomic() の「全部か無か」はどう効くのか
複数レコードを1件ずつ保存すると、途中で失敗したとき半端なデータが残る。transaction.atomic() で「全部成功か、全部なし」にまとめ、bulk_create と合わせて整合性を守る方法の学習まとめ。
サービス層関数の owner フィルタ漏れ
ビュー層に依存した「呼び出し元を信じる」サービス層関数が、構造変更で穴を開けるパターンと Defense in Depth の考え方の学習まとめ
Django の SECRET_KEY を環境変数で必須化する — base のデフォルトと本番の二層構造
SECRET_KEY に開発用デフォルトを残しつつ、本番・開発設定で環境変数を必須にして上書きする『二層構造』。重要設定を『未設定なら起動を止める』にする理由を、綴Photo の実コードでまとめる。
Flutter で「画面全体で揃えたい数字」をどこからでも呼び出せるようにした話
フォントサイズ・余白・サイズなど「画面全体で揃えたい数字」を1箇所にまとめて、どの画面からでも呼び出せるようにした実装記録。Flutter の InheritedWidget を使った自前デザイントークンの話
昔の写真に「今日の気持ち」を書き足せるアプリを作っている。重ねる UX とその実装
1枚の写真に複数の日記エントリを後から重ねていく機能。なぜそれを作ろうと思ったか、UX と実装の方針
写真アプリのローカル DB をどう設計したか。「将来の同期」をどこまで先読みするか問題
綴Photo のローカル DB を複数テーブル構成にした理由と、まだ実装していない同期機能のためにどこまで先読みしたかの記録
「また今度」ボタンの裏側にある設計。スキップした写真をちゃんと戻す仕組み
スワイプして「Pass」した写真を消さずに後から再浮上させる、その UX と DB 設計の話
カメラロール 5000 枚でも速い写真スキャンを Flutter で作った話
端末の写真ライブラリから「まだ綴っていない写真」を集める処理。新しい順スキャンと cutoff 打ち切りで実用速度に乗せた話
画面ごとのサイズ違いは、表を1枚作って選ぶだけにする — Flutter のデータクラスで isDesktop をウィジェットから追い出した話
ウィジェットの中に if (isDesktop) が散らかった結果、サイズ定義をデータクラスの static const プリセットに集約した話
写真とテキストで罫線がぶつ切りになる。1枚のノートみたいに繋げた — Flutter CustomPainter
写真エリアとテキストエリアで罫線がぶつ切りになる問題を、CustomPainterで線の始まる位置をそろえて繋げた話
DELETE 204 と 401/403 ステータスコードずれ
DELETE 204 No Content の JSON パースエラーと、401/403 ステータスコードずれの学習まとめ
Django save(update_fields=...) で既存データを壊しかけた話
Django の save(update_fields=...) を間違えると、エラーも出ずにデータが NULL で上書きされる。「動いているのに壊れている」設計ミスの学習まとめ
該当する記事がありません。