綴Photo の勉強ノートに戻る
開発記録2026.05.22

デスクトップで『共有』が出るのは違う — 同じ『書き出す』でもプラットフォームで導線を変える

Fluttershare_plusfile_pickerプラットフォームプライバシー綴Photo

綴Photo には「データをエクスポート」機能がある。綴った言葉を JSON で書き出して控えを残すためのもの(写真本体は含まない、テキストの控え)。モバイルでは問題なく動いていたが、デスクトップ(Windows)で試したら様子がおかしいことに気づいた。

「データをエクスポート」を押すと、出てくるのが Windows の共有ダイアログ——メールや他アプリ宛てに「共有」する画面だった。JSON ファイルをどこかに保存したいのに、保存する導線がどこにもない。

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

  • 同じ「書き出す」でも、モバイルとデスクトップで自然な操作(イディオム)が違います
  • share_plus の共有はモバイルの作法。デスクトップは「名前を付けて保存」
  • ついでに: 自分のデータの書き出しでも、絶対パスは漏れます

何が起きていたか

エクスポートはこうなっていた。

dart
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/tsuzuphoto_export.json');
await file.writeAsString(jsonString);
 
await Share.shareXFiles(
  [XFile(file.path, mimeType: 'application/json')],
  subject: '綴Photo データエクスポート',
);

一時ファイルを書いて Share.shareXFiles で「共有」します。モバイルではこれが正解です。

JSON はメモリ。SQLite の Photos / Meanings から組み立て、DB には書き戻さない。一時ファイル Documents / tsuzuphoto_export.jsonどちらも Share.shareXFiles へ渡すモバイル共有シートファイルに保存 / AirDrop / メール保存先を選べる。意図と合うデスクトップWindows の共有 UI近距離共有 / メール / アプリで開くディスクへ保存する導線が無い

直し: デスクトップは「名前を付けて保存」

OS で分岐して、デスクトップは file_picker の saveFile(名前を付けて保存ダイアログ)を使う形にした。

dart
if (PlatformUtils.isDesktopOS) {
  // デスクトップ: 保存先を選ぶダイアログ
  final savePath = await FilePicker.platform.saveFile(
    dialogTitle: 'データを保存',
    fileName: 'tsuzuphoto_export.json',
    type: FileType.custom,
    allowedExtensions: ['json'],
  );
  if (savePath == null) return; // キャンセル
  await File(savePath).writeAsString(jsonString);
  // 共有もしないし一時ファイルも作らない
  return;
}
 
// モバイル: 従来どおり共有シート
// (一時ファイルを書いて Share.shareXFiles → 共有後に削除)
同じ JSON はメモリ。SQLite には書き戻さない。分岐は OS。PlatformUtils.isDesktopOSデスクトップ名前を付けて保存file_picker の saveFile選んだパスへ直書き一時ファイルも共有も無しファイルは選んだ場所のディスクモバイル共有シートのままShare.shareXFiles一時ファイルを書いて渡す共有後に一時ファイルを消すファイルの行き先は共有先次第

実機で試すと、デスクトップでちゃんと保存ダイアログが出て、選んだ場所に JSON が落ちるようになった。

ついでに直したプライバシー: 絶対パスを残さない

保存された JSON の中身を確認していて、もう1つ気づいた。写真の参照として localPath(デスクトップだと絶対パス)がそのまま入っていた。

json
{ "localPath": "C:\\Users\\<ユーザー名>\\Pictures\\2024\\..." }
同じ写真の住所でも、置く先で中身が違う。端末 SQLite PhotoslocalPath は絶対パスのままC:\Users\名前\Pictures...clientPhotoId は UUID閉じても残る。書き出しでは触らない書き出し JSONlocalPath はファイル名だけphoto_001.jpgclientPhotoId は同じ UUIDユーザーが保存したファイルに入る再突き合わせは UUID。フルパスは JSON に載せない

これは自分のデータを自分で書き出すものなので危険度は低いです。localPath はファイル名だけにマスクして書き出す形に変えた。

dart
'localPath': m.photo.localPath == null
    ? null
    : _fileNameOnly(m.photo.localPath!), // 絶対パス → "photo_001.jpg"

「自分のデータだから何が入っていてもいい」ではなく、「その用途に必要な情報だけ書き出す」に寄せた。書き出したファイルがどこへ行く(クラウド同期・うっかり共有)か分からない以上、最初から余計なものを入れないのが堅いです。

学びポイント

  • 「クロスプラットフォームな1つの共有 API」で済ませると、デスクトップでは保存になりません。分岐は PlatformUtils.isDesktopOS
  • 自分のデータの書き出しでも、用途に必要な情報だけ載せます

おわりに

呼べることと自然に使えることのズレは、実機(しかもデスクトップ)で触らないと引っかかりません。エミュレータやモバイルだけ見ていたら見逃していた。


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