maui-file-handling について
.NET MAUIでファイルを扱う開発者向けのスキルです。プラットフォームごとのピッカー形式や、パス・権限・ストリームの注意点を説明します。
主な機能
- 01OpenReadAsync による選択ファイルの読み取り
- 02プラットフォーム別のファイル形式設定
- 03編集前にバンドルファイルをコピー
- 04クロスプラットフォームのディレクトリパス利用
- 05ピッカーのキャンセルと Android 権限の確認
活用シーン
- 01クロスプラットフォームのファイル選択と読み取り
- 02Android、iOS、macOS、Windows向けフィルター設定
- 03バンドルアセットの保存と編集
- 04Androidのストリーム長や権限の問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-file-handlingインストール後、Claude Code で/maui-file-handlingを入力して読み込まれたか確認します。
同梱ファイルreferences/file-handling-api.md
詳細
.NET MAUI のファイル処理
重要:常に OpenReadAsync() を使用し、FullPath は使用しないでください
一部のプラットフォーム(特に Android)では、ファイルシステムのパスではなくコンテンツ URIが返されます。FullPath を直接読み取ると、例外が発生するか、データが空になります。
// ❌ Breaks on Android — FullPath may be a content:// URI
var result = await FilePicker.Default.PickAsync();
var bytes = File.ReadAllBytes(result.FullPath);
// ✅ Works on all platforms
var result = await FilePicker.Default.PickAsync();
if (result is not null)
{
using var stream = await result.OpenReadAsync();
// process stream
}
プラットフォームごとに異なるファイル形式に注意
カスタムの FilePickerFileType では、プラットフォームごとに異なる形式が使用されます。
形式を取り違えると、ファイルピッカーにファイルが表示されなかったり、クラッシュしたりします。
| プラットフォーム | 形式 | 例 |
|---|---|---|
| Android | MIME タイプ | "application/json" |
| iOS / macOS | UTType 識別子 | "public.json" |
| Windows | ドットで始まる拡張子 | ".json" |
// ❌ Using file extensions for Android — picker shows nothing
{ DevicePlatform.Android, new[] { ".json", ".txt" } }
// ✅ Correct MIME types for Android
{ DevicePlatform.Android, new[] { "application/json", "text/plain" } }
よくある落とし穴
バンドルされたファイルは読み取り専用です
Resources/Raw のアセットは、実行時に変更できません。書き込もうとすると、例外が発生します(プラットフォームによっては何も通知されずに失敗します)。
// ❌ Trying to write to a bundled file
var path = "data.json"; // inside Resources/Raw
File.WriteAllText(path, newContent); // fails
// ✅ Copy to AppDataDirectory first, then modify
string targetPath = Path.Combine(FileSystem.Current.AppDataDirectory, "data.json");
if (!File.Exists(targetPath))
{
using var source = await FileSystem.Current.OpenAppPackageFileAsync("data.json");
using var dest = File.Create(targetPath);
await source.CopyToAsync(dest);
}
// Now safe to read/write targetPath
バンドルされたサブディレクトリはフラット化されます
一部のプラットフォームでは、Resources/Raw/subdir/file.txt は単に file.txt になります。サブディレクトリ構成にかかわらず、一意のファイル名を使用してください。
再ビルド時に iOS のサンドボックスパスが変わります
iOS のサンドボックスパスにはアプリの GUID が含まれ、クリーンビルドのたびに変わります。絶対パスをハードコードすると、気付かないうちに動作しなくなります。
// ❌ Hard-coded path — breaks after clean rebuild
var path = "/var/mobile/.../Documents/data.json";
// ✅ Always use the FileSystem helper
var path = Path.Combine(FileSystem.Current.AppDataDirectory, "data.json");
Android:バンドルされたストリームには Length がありません
OpenAppPackageFileAsync は、.Length を呼び出すと NotSupportedException が発生するストリームを返すことがあります。サイズが必要な場合は、MemoryStream にコピーしてください。
// ❌ Throws on Android
using var stream = await FileSystem.Current.OpenAppPackageFileAsync("data.json");
var size = stream.Length; // NotSupportedException
// ✅ Copy first if you need the length
using var stream = await FileSystem.Current.OpenAppPackageFileAsync("data.json");
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var size = ms.Length;
Windows:仮想化されたファイルシステム
パッケージ化されたアプリでは、書き込み先が %AppData% などの従来のパスに通知されずにリダイレクトされます。クロスプラットフォームで確実に使用できるパスとして、常に AppDataDirectory と CacheDirectory を使用してください。
キャンセル時、FilePicker は null を返します
// ❌ NullReferenceException when user cancels
var result = await FilePicker.Default.PickAsync();
using var stream = await result.OpenReadAsync();
// ✅ Always null-check
var result = await FilePicker.Default.PickAsync();
if (result is null) return;
using var stream = await result.OpenReadAsync();
Android の権限(API 33 以降の変更)
Android 13 では、READ_EXTERNAL_STORAGE が用途別のメディア権限に置き換えられました。API 33 以降で従来の権限を使用しても、何も許可されません。
| Android のバージョン | 必要な権限 |
|---|---|
| ≤ 12 (API 32) | READ_EXTERNAL_STORAGE |
| ≥ 13 (API 33) | READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO |
チェックリスト
-
OpenReadAsync()を使用する —FullPathを直接読み取らない - プロパティにアクセスする前に、
FilePickerの結果が null でないことを確認する - カスタムの
FilePickerFileTypeでは、プラットフォームごとに正しい形式(MIME / UTType / 拡張子)を使用する - バンドルされたファイルを変更する前に
AppDataDirectoryにコピーする -
Resources/Rawでは一意のファイル名を使用する(サブディレクトリはフラット化されます) - Android マニフェストで、対象 API レベルに適した権限を宣言する
- macOS:App Sandbox エンタイトルメントにファイルアクセスを含める