.NET MAUI 設定ファイルが更新で消える原因と対策|Settings.txtをAppDataDirectoryで保持する方法

.NET MAUI アプリでユーザー設定を Settings.txt に保存しているのに、Google Play で更新したら消えてしまう――原因は「書き換えてはいけない領域」に保存している可能性が高い。更新に強い保存先と、既定設定+上書きの設計、再インストール時の扱いまで整理する。

目次

症状:ストア更新後に Settings.txt が消える・初期化される

.NET MAUI(Android / Windows)アプリで、色・ID・最終状態などのユーザー設定を Settings.txt に読み書きしている。開発中(エミュレーターやローカル配布)では問題が出ないのに、Google Play の内部テスト/クローズドテストで新バージョンへ更新した瞬間に Settings.txt が初期化される・消える。この現象は、保存先が「アプリのインストール領域」寄りになっていると起きやすくなります。

原因の本質:置き換わる領域と、残すべき領域が混ざっている

ストア更新は、見た目は「上書き」ですが、内部的にはアプリ本体(パッケージ)の差し替えです。パッケージ内に含まれるファイルや、インストールディレクトリ配下にあるファイルは、更新時に置き換え対象になり得ます。つまり、アプリに同梱した Settings.txt をそのまま書き換えて運用していると、更新で同梱ファイルが新しいものに置き換わり、結果として「消えた」「初期化された」と見えることがあります。

保存場所の考え方更新(ストア配信)アンインストールよくある誤解
アプリに同梱される領域(インストール領域、パッケージ内)置き換わる可能性が高い消える「動いたから保存できるはず」と思い込みやすい
アプリ専用データ領域(AppData / Sandbox)基本的に保持される消える(原則)「更新で消えるのは仕様」と誤解されがち
キャッシュ領域(Cache)残ることもあるが保証しない消える「小さい設定だからキャッシュで十分」と判断してしまう
外部ストレージや共有フォルダ残るが環境・権限・ユーザー操作に左右される残ることが多い「残る=安全」と考え、個人情報や機密を置いてしまう

今回のような「色・ID・最終状態」などのユーザー設定は、まさに更新後も残したいユーザーデータです。したがって、保存先はアプリ専用データ領域に寄せ、アプリ同梱領域とは明確に分離するのが正攻法になります。

結論:ユーザー設定は FileSystem.Current.AppDataDirectory に保存する

.NET MAUI では、プラットフォームごとの「正しいデータ保存先」を直接ハードコードするより、MAUI が提供するファイルシステム API を使う方が安全です。特にユーザー設定のような永続データは、FileSystem.Current.AppDataDirectory を第一候補にします。

実装の方向性はシンプルです。保存パスを AppDataDirectory 配下に固定し、そのパスへ読み書きします。

var path = Path.Combine(FileSystem.Current.AppDataDirectory, "Settings.txt");
await File.WriteAllTextAsync(path, content);

この「AppDataDirectory」は、Android ならアプリの内部ストレージ(アプリ専用ディレクトリ)、Windows ならパッケージアプリの LocalState など、OS がアプリごとに隔離して管理する領域にマッピングされます。ストア更新でアプリ本体が差し替わっても、通常はこのデータ領域は保持されます。

Environment.SpecialFolder.LocalApplicationData ではダメなのか

Environment.SpecialFolder.LocalApplicationData でも動くことはありますが、アプリの配布形態やプラットフォームによって「実体の場所」が変わり、更新後に別の場所を読みに行ってしまう事故が起こりがちです。

  • Windows は「パッケージ版(Store / MSIX)」と「非パッケージ版(デバッグ実行など)」でデータ保存の基準ディレクトリが変わることがある
  • 相対パスや作業ディレクトリ依存の実装だと、デバッグでは読めてもストア配布では読めない

MAUI の FileSystem.Current.AppDataDirectory は、この差異を吸収してくれるため、ストア更新を含む実運用ではトラブルが減ります。

やってはいけない設計:同梱ファイルを直接書き換える

更新で消えるケースの多くは、以下のどれかに該当しています。

  • アプリに同梱した Settings.txt(Content / Assets / Resources など)を、そのまま編集して保存している
  • 実行ファイルが置かれているディレクトリ(インストール先)に Settings.txt を生成している
  • 相対パスでファイルを扱っており、デバッグ実行とストア配布で「実体の場所」が変わっている

開発環境では「たまたま書けてしまう」ことがあります。しかしストア配信版では、パッケージ領域は読み取り専用だったり、更新で置き換わったり、そもそも別の場所に配置されます。結果、保存したはずの Settings.txt が更新で消えたり、読み込めなくなったりします。

ポイントは次の分離です。

  • アプリに同梱する既定設定:アプリ側が提供する初期値。更新で変わってよい
  • ユーザーが変更する設定データ:ユーザー操作で変化する値。更新後も残したい

安定運用の基本パターン:「既定 + 上書き」の二段構え

「更新に強い」だけでなく、運用中の事故(設定ファイル破損、仕様変更、初回起動時の欠損)にも強くするなら、既定(デフォルト)+ユーザー上書きの2段構えが最も安定します。

ファイル置き場所役割更新時の挙動
Settings.default.txt(例)アプリ同梱(パッケージ内)既定値。新機能追加時の初期設定もここに反映更新で置き換わる(それでOK)
Settings.txt(ユーザー設定)AppDataDirectoryユーザーが変更した値。最後に読み込んで上書き更新で保持されることが多い

読み込みの流れは、次の「後勝ち(後に読み込んだ方が優先)」で揃えます。

  • まず既定設定(同梱)を読み込む
  • 次にユーザー設定(AppData)を読み込む
  • 同じキーがあればユーザー設定で上書きする

JSON を使っている場合、.NET の Configuration で AddJsonFile を「既定→ユーザー」の順に足すと、後から追加した設定が優先される考え方になり、イメージが掴みやすいでしょう。テキスト形式でも同様に、キー=値の辞書として後勝ちでマージすれば同じ設計になります。

実装例:Settings.txt を AppDataDirectory に保存する(初回作成・読み込み・上書き)

ここでは「Settings.txt を AppDataDirectory に置く」ことを前提に、初回起動時に既定ファイルから生成し、以後はユーザー設定として更新し続ける例を示します。アプリ更新時も、AppDataDirectory 側の Settings.txt が残っていれば維持される設計です。

既定ファイルをアプリに同梱する方法(MauiAsset の例)

既定設定をパッケージに同梱して読み取るなら、ファイルをプロジェクトに追加し、ビルドアクションを MAUI の資産として扱える形式にします(例:MauiAsset)。読み込みは FileSystem.Current.OpenAppPackageFileAsync を使うのが安全です。

ファイル名が衝突すると混乱しやすいので、同梱側は Settings.default.txt、ユーザー側は Settings.txt のように役割を名前で分けると、運用が一気に楽になります。

読み込みの例(AppData を優先し、なければ既定をコピー)

using System.Text;

public sealed class SettingsStore
{
private const string DefaultFileInPackage = "Settings.default.txt";
private const string UserFileName = "Settings.txt";


private readonly string _userPath;

public SettingsStore()
{
    _userPath = Path.Combine(FileSystem.Current.AppDataDirectory, UserFileName);
}

public async Task<string> LoadAsync()
{
    // 1) ユーザー設定(AppData)があればそれを最優先
    if (File.Exists(_userPath))
    {
        return await File.ReadAllTextAsync(_userPath, Encoding.UTF8);
    }

    // 2) なければ、同梱の既定設定を読み込む
    using var stream = await FileSystem.Current.OpenAppPackageFileAsync(DefaultFileInPackage);
    using var reader = new StreamReader(stream, Encoding.UTF8);
    var defaultContent = await reader.ReadToEndAsync();

    // 3) 初回起動用に AppData に作成して返す
    await SaveAsync(defaultContent);
    return defaultContent;
}

public async Task SaveAsync(string content)
{
    Directory.CreateDirectory(FileSystem.Current.AppDataDirectory);

    // 途中でアプリが落ちてもファイルが壊れにくいよう、一時ファイル経由で置き換える
    var tmp = _userPath + ".tmp";
    await File.WriteAllTextAsync(tmp, content, Encoding.UTF8);

    if (File.Exists(_userPath))
    {
        File.Delete(_userPath);
    }

    File.Move(tmp, _userPath);
}


}

ポイントは 3 つです。

  • 保存先を AppDataDirectory に固定する(更新に強い)
  • 初回起動で「同梱の既定設定」を AppData にコピーする(欠損に強い)
  • 書き込みは一時ファイル→置き換えで行う(破損に強い)

設定形式の例(キー=値のテキスト)

Settings.txt をプレーンテキストで運用する場合、最低限「キーと値」を安定して扱える形式にしておくと、将来の項目追加や移行が楽になります。

ThemeColor=#FF00FF
UserId=12345
LastScreen=Home
LastState=Expanded

読み込み時は、行を分割して辞書にし、既定→ユーザーで後勝ちマージします。たとえばキー=値を辞書化し、ユーザー側で上書きする最小実装は次のように書けます。

public static Dictionary<string, string> ParseKeyValue(string text)
{
    var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

    foreach (var raw in text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries))
    {
        var line = raw.Trim();
        if (line.StartsWith("#")) continue; // コメント行

        var parts = line.Split('=', 2);
        var key = parts[0].Trim();
        var value = parts.Length > 1 ? parts[1].Trim() : "";

        if (key.Length == 0) continue;
        dict[key] = value;
    }

    return dict;
}

public static Dictionary<string, string> Merge(Dictionary<string, string> defaults, Dictionary<string, string> user)
{
    var merged = new Dictionary<string, string>(defaults, StringComparer.OrdinalIgnoreCase);
    foreach (var kv in user)
    {
        merged[kv.Key] = kv.Value; // 後勝ち
    }
    return merged;
}

この形にしておけば、既定側に新しいキーが増えても、ユーザー側に無い限り既定値が効きます。逆にユーザーが変更したキーだけが上書きされるため、「更新で既定が変わってもユーザーの好みは維持される」挙動になります。

用途別:どのディレクトリを使うべきか

「とりあえず保存できる場所」は複数ありますが、更新耐性・安全性・意図の明確さで選ぶのがコツです。

用途推奨ディレクトリ理由
更新後も残したいユーザー設定AppDataDirectoryOS がアプリ専用領域として管理し、更新で保持されやすい
一時データ(再生成できる、消えてもよい)CacheDirectoryOS 都合で消える可能性がある前提で使う。設定保存には不向き
ユーザーが外部へ持ち出したいファイル(業務用途のエクスポート等)ユーザーが選べる場所(ファイルピッカー等)アプリ領域に閉じず、ユーザーの管理下に置ける

プラットフォーム別:更新で残る?残らない?の基本整理

「更新で残るか」は、保存先がアプリ専用データ領域(AppData/Sandbox)である限り、多くの OS で同じ方向性です。ただし例外条件(アプリID変更、署名変更、ユーザーがデータ削除など)もあるため、動作の前提を文章で固めておくと事故が減ります。

プラットフォームAppDataDirectory(アプリ専用データ)アプリ同梱領域(パッケージ内)注意点
Android(Google Play)通常は更新で保持更新で置き換えパッケージ名(applicationId)や署名が変わると別アプリ扱いになり、結果としてデータが引き継がれない
Windows(Microsoft Store / MSIX)通常は更新で保持更新で置き換えパッケージアプリは LocalState 等に保存。デバッグ実行(非パッケージ)と保存先が異なる場合がある
iOS(App Store)通常は更新で保持更新で置き換え(Bundle は読み取り専用)バックアップ対象ディレクトリの扱い(iCloud)を意識。機密は SecureStorage を検討
macOS(Mac App Store)通常は更新で保持更新で置き換えサンドボックス(Container)配下に保存する。インストール先への書き込み前提は避ける
Tizen など基本は更新で保持の設計が多い更新で置き換えプラットフォーム固有のデータ領域を MAUI API 経由で使うことで差異を吸収しやすい

結局のところ、ストア更新でデータを守りたいなら「どの OS でも、ユーザーが書き換えるものは AppData に置く」が最短ルートです。MAUI の FileSystem.Current.AppDataDirectory は、その最短ルートをクロスプラットフォームで提供してくれます。

補足:更新で残っても「アンインストール→再インストール」では基本消える

誤解が多いポイントなので、あえて強調します。AppDataDirectory のファイルは、更新では残ることが多い一方、アンインストールされると原則として消えます。これは OS が「そのアプリのデータ領域」をアプリ削除と同時にクリーンアップするためです。

もし「削除して入れ直しても設定を残したい」要件があるなら、ローカルファイル保存だけでは完結しません。次のいずれかを検討します。

  • クラウド同期:アカウントログインが前提なら最も確実。自前API、Firebase、Microsoft Graph など設計に合わせて選ぶ
  • OS のバックアップ機構:Android の Auto Backup などを利用し、設定ファイルをバックアップ対象に含める
  • エクスポート/インポート:ユーザーにファイルとして保存・復元してもらう(業務アプリで採用されやすい)

「更新で残れば十分」なのか、「再インストールでも残したい」なのかで、設計と工数が大きく変わります。前者は AppData 保存でほぼ解決しますが、後者はクラウドやバックアップをセットで考えるのが現実的です。

トラブルシュート:それでも更新で消える場合に見るチェックリスト

AppDataDirectory に変えたのに、なお「更新で消える」「初回扱いになる」場合は、保存先以外の要因が混ざっている可能性があります。ストア配信で起きやすいチェック項目をまとめます。

チェック項目起きる症状確認のヒント
アプリID(Android の package / Windows の PFN など)を変更していないか更新ではなく「別アプリ扱い」になり、データが引き継がれないストア側で同一アプリとして更新されているか、端末でアプリが2つ並んでいないか確認
署名(keystore / 証明書)が変わっていないか更新インストールできず、実質アンインストール→インストールになっているリリース署名を固定。Play App Signing を使う場合も移行手順を確認
キャッシュ領域に保存していないか(CacheDirectory など)更新やOS都合で消えるパスが Cache 配下になっていないかログ出力して確認
初期化コードが「毎回」既定ファイルで上書きしていないか更新後に設定が初期値へ戻る「ファイルが存在しない時だけ作成」になっているか見直す
書き込み失敗(例外)を握りつぶしていないか保存できたつもりで次回起動時に欠損扱いになる例外ログ、ストレージ残量、並行書き込みを確認
複数スレッド/複数箇所から同時に保存していないか破損、空ファイル化、更新直後に欠損SemaphoreSlim などで保存処理を直列化

特に「初期化コードの上書き」は盲点になりがちです。たとえば、起動時に毎回「同梱 Settings.txt をコピー」していると、ユーザーが変更した AppData 側の Settings.txt を毎回上書きしてしまいます。初回だけ作る、または既定+ユーザーを分けてマージする、のどちらかに寄せると事故が減ります。

運用をさらに安定させる工夫:バージョン管理と移行(マイグレーション)

アプリを継続的に更新していると、設定項目が増えたり、名前が変わったり、値の意味が変わったりします。更新後も設定を保持できても、形式が変わると読み込みに失敗して「初期化」に見えることがあります。そこで次の工夫が効きます。

  • 設定ファイルに schemaVersion を持たせる(例:SchemaVersion=2)
  • 読み込み時に schemaVersion を見て、必要なら移行処理を実行する
  • 不明なキーは無視し、既定値で補完する(壊れにくい)

テキスト形式でも、先頭行に SchemaVersion を入れておくだけで効果があります。JSON なら "schemaVersion": 2 のように持たせるだけです。移行処理は「古いキーを新しいキーへ写す」「値の形式を変換する」など、必要な部分だけで十分です。

ファイル保存以外の選択肢:Preferences / SecureStorage / データベース

Settings.txt の運用が悪いわけではありません。ただ、用途によっては別の仕組みがシンプルで安全なこともあります。代表的な選択肢を整理します。

手段向いている用途注意点
Preferences数個〜数十個の軽い設定(色、最終画面、フラグなど)複雑な構造や大量データには不向き。キー設計を誤ると管理が難しくなる
SecureStorageトークン、パスワード、機密情報など保存できるサイズに限界がある。バックアップや移行の扱いはOS依存
ファイル(AppDataDirectory)設定項目が多い、ユーザーがエクスポートしたい、ログに近い形式で残したい書き込みの原子性、破損対策、移行(schemaVersion)を考えると安定
SQLite などDB履歴、一覧、検索が必要なデータ(最終状態だけではなく時系列を残す等)導入コストが上がる。設定のためだけに入れると過剰になりやすい

「色・ID・最終状態」なら Preferences でも十分なケースが多いですが、設定が増えて構造化したくなったら AppDataDirectory へのファイル保存(JSON など)が自然な移行先です。一方、認証トークンのような機密は SecureStorage を優先し、Settings.txt に混ぜない方が安全です。

まとめ:ストア更新に強い Settings 設計の要点

  • ストア更新で消える Settings.txt は、保存先がアプリ同梱領域寄りになっている可能性が高い
  • ユーザーが変更する設定は、FileSystem.Current.AppDataDirectory に保存する
  • 既定設定(同梱)とユーザー設定(AppData)を分け、後勝ちで上書きする2段構えが安定
  • 更新で残っても、アンインストール→再インストールでは基本消える。残したいならクラウド同期やバックアップを検討
  • 初期化コードの「毎回コピー」や、アプリID・署名変更など、保存先以外の要因もチェックする

Settings.txt を「どこに置くか」と「どう読み込むか」を整理するだけで、Google Play の更新でも Windows Store でも、設定保持の再現性は大きく上がります。まずは AppDataDirectory への移行と、既定+上書きの設計から始めるのが最短です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次