.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;
}
この形にしておけば、既定側に新しいキーが増えても、ユーザー側に無い限り既定値が効きます。逆にユーザーが変更したキーだけが上書きされるため、「更新で既定が変わってもユーザーの好みは維持される」挙動になります。
用途別:どのディレクトリを使うべきか
「とりあえず保存できる場所」は複数ありますが、更新耐性・安全性・意図の明確さで選ぶのがコツです。
| 用途 | 推奨ディレクトリ | 理由 |
|---|---|---|
| 更新後も残したいユーザー設定 | AppDataDirectory | OS がアプリ専用領域として管理し、更新で保持されやすい |
| 一時データ(再生成できる、消えてもよい) | CacheDirectory | OS 都合で消える可能性がある前提で使う。設定保存には不向き |
| ユーザーが外部へ持ち出したいファイル(業務用途のエクスポート等) | ユーザーが選べる場所(ファイルピッカー等) | アプリ領域に閉じず、ユーザーの管理下に置ける |
プラットフォーム別:更新で残る?残らない?の基本整理
「更新で残るか」は、保存先がアプリ専用データ領域(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 への移行と、既定+上書きの設計から始めるのが最短です。

コメント