.NET MAUI で作ったアプリが Windows では動くのに、Android で起動すると System.PlatformNotSupportedException で落ちる――この症状は、チュートリアル通りに進めた人ほど遭遇しがちです。多くの場合、原因は MAUI 本体ではなく「参照している NuGet パッケージや実装が Windows 前提」な点にあります。本記事では例外の読み解き方から、OpenAI/ChatGPT の API を Android でも確実に動く形で呼び出す設計・実装まで、再発防止の観点で整理します。
起きていること:Android 実行時に System.PlatformNotSupportedException が投げられる
System.PlatformNotSupportedException: Operation is not supported on this platform. は、その名の通り「その API は今動いているプラットフォームでは未サポートです」という合図です。.NET MAUI は 1 つのプロジェクトで複数プラットフォーム(例:Windows / Android / iOS)をターゲットにできますが、すべての .NET API や NuGet が全プラットフォームで同じように動くわけではありません。
特に「Windows 向けのチュートリアル」をベースに作った場合、Windows では問題にならない依存関係が Android では一気に表面化し、起動直後や API 呼び出し時に例外として落ちます。
なぜ Android で動かないのか:原因はほぼ“依存ライブラリ”にある
今回の前提では、Microsoft の「ChatGPT を使ったレコメンドアプリ」系チュートリアルをベースにし、OpenAI 用の .NET ラッパー(例:openai-dotnet など)を利用しています。こうしたラッパーは便利な一方で、作者が想定している実行環境が サーバー/デスクトップ中心だと、モバイル(Android/iOS)で未サポートの API を内部で呼んでしまうことがあります。
つまり「MAUI だからダメ」ではなく、たまたま踏んだ依存先が Android 未対応であることが根本原因になりがちです。実際の現場でも、外部 API 連携(AI/決済/地図など)で “便利ラッパー” を入れた瞬間に Android だけ落ちるケースは珍しくありません。
| よくある原因パターン | 何が起きるか | Android での典型的な結果 | 初動の対処 |
|---|---|---|---|
| ライブラリが Windows 専用ターゲット(net8.0-windows 等)を含む | ビルドは通っても、実行時に Windows 前提の実装が走る | PlatformNotSupportedException / TypeLoadException | NuGet の対応 TFM を確認し、モバイル対応版に替える |
| 暗号化/保護 API(例:Windows の DPAPI 相当)を内部で使用 | キー保護や資格情報保存でプラットフォーム固有 API を呼ぶ | PlatformNotSupportedException が OpenAI 呼び出し前後で発生 | モバイル向けの安全な保存(SecureStorage 等)に寄せる |
| Windows のファイルパスやレジストリなど OS 固有機能 | C:\ やレジストリ参照など | Android で即死、または設定読み込みで例外 | プラットフォーム分離(条件コンパイル/Partial) |
| WinUI/WPF 前提の UI・依存 DLL | UI 層に Windows 専用参照が混入 | 起動時にクラッシュ | 共有層を “純 MAUI” に保つ |
まずやるべき切り分け:例外メッセージより “スタックトレース” を見る
PlatformNotSupportedException はメッセージが抽象的なので、解決の近道は 例外が投げられた「最初の自分のコード行」を特定することです。Visual Studio の例外表示でスタックトレースを展開し、以下を確認します。
- 例外が投げられた直前に呼んでいるメソッド名(ライブラリ側の型/名前空間)
- 自分のプロジェクトのファイル名と行番号(ここが起点)
- どの NuGet から呼ばれているか(パッケージ名が出ることも多い)
ここで OpenAI ラッパーの内部(例:認証ヘッダー生成、設定読み込み、ログ出力、ストリーミング処理など)に落ちているなら、ライブラリが Android で未対応、もしくは Android 用の実装が欠けている可能性が高いです。
依存パッケージを“犯人捜し”する具体的な手順
原因の NuGet が 1 つとは限りません。特にラッパー系は「依存の依存(トランジティブ依存)」が多く、そこに Windows 専用が混ざっていることもあります。次の順に潰すと早いです。
- 例外発生箇所を特定:スタックトレースで自分のコード行まで戻す
- その周辺で呼んでいるライブラリを洗い出す:AI クライアント、設定読み込み、保存処理など
- パッケージ一覧を確認:直接参照だけでなくトランジティブも含めて見る
- 一度パッケージを外して起動確認:外して動くなら依存が原因と確定しやすい
CLI を使える環境なら、トランジティブ依存まで含めた一覧が一発で出ます。
dotnet list package --include-transitive
ライブラリ側の“Android 対応”を見極めるポイント
「この NuGet は Android でも動くのか?」を判断するには、次の観点が有効です。ここを押さえるだけで、PlatformNotSupportedException で詰む確率が大きく下がります。
- 対応 TFM(ターゲットフレームワーク):
net8.0-android/net7.0-androidがあるか、またはnetstandard2.0中心で OS 依存 API を避けているか - README / ドキュメントの前提環境:「Windows のみ」「ASP.NET 前提」などの記述がないか
- Issue の傾向:Android/iOS の不具合報告が放置されていないか、メンテ状況は活発か
- 依存関係:Windows 専用のパッケージを引っ張っていないか(UI/暗号/OS 機能系に注意)
補足:「.NET ならどこでも動くはず」と思いがちですが、実際には NuGet が内部で OS 固有 API を呼ぶだけで、モバイルは簡単に詰みます。逆に言えば、対応 TFM と前提環境を確認する習慣さえ付ければ、同種トラブルはかなり防げます。
解決方針は2つ:対応済みライブラリに乗り換えるか、HttpClient で直叩きする
現実的な選択肢は次の2本立てです。急いで動かすなら「直叩き」が強く、長期運用では「ライブラリの対応状況」を見て戻すのが定石です。
| 方針 | メリット | デメリット | おすすめの場面 |
|---|---|---|---|
| Android 対応の OpenAI .NET ライブラリを使う | 型安全・機能が揃う(ストリーミング、ツール呼び出しなど) | 対応状況に依存。更新で破壊的変更が起こることも | ライブラリが net8.0-android などを明確にサポートしている |
| HttpClient で REST API を直接呼ぶ | プラットフォーム依存を最小化。MAUI の共有コードに置ける | レスポンス解析やエラー処理を自前で書く必要 | まず動かしたい/原因がライブラリ側っぽい/運用を自分で握りたい |
当面の最短ルート:HttpClient で OpenAI REST API を呼び出す
Android で確実に動かすには、MAUI の共有層(net8.0 部分)に HttpClient ベースのサービスを置くのが堅実です。ここでは「Chat 形式で 1 回問い合わせて 1 回返す」最小構成を、実運用を見据えて少し丁寧に書きます。
実装のポイント
- HttpClient は使い回す(毎回 new するとソケット枯渇の原因になり得る)
- CancellationToken を通す(画面遷移やキャンセルで止められる)
- タイムアウトとエラー本文を必ず読む(429/401/500 の切り分けが速くなる)
- レスポンスはまず ログに残せる形で保持(ただし API キーは絶対に出さない)
サービスクラス例:.NET MAUI 共有コードで動く
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public sealed class OpenAiChatService
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
public OpenAiChatService(HttpClient httpClient, string apiKey)
{
_httpClient = httpClient;
_apiKey = apiKey;
}
public async Task<string> AskAsync(string userMessage, CancellationToken cancellationToken = default)
{
// エンドポイントとモデル名は、利用している API 仕様・契約に合わせて調整してください
var endpoint = "https://api.openai.com/v1/chat/completions";
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var requestBody = new
{
model = "gpt-3.5-turbo",
messages = new object[]
{
new { role = "user", content = userMessage }
},
temperature = 0.7
};
var json = JsonSerializer.Serialize(requestBody);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseContentRead,
cancellationToken
);
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
// ここで responseText をログに残すと原因究明が速い(キーや個人情報はマスクする)
throw new HttpRequestException(
$"OpenAI API error: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseText}"
);
}
// 必要最小限のパース(choices[0].message.content を取り出す)
using var doc = JsonDocument.Parse(responseText);
var root = doc.RootElement;
var content = root
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return content ?? string.Empty;
}
}
依存性注入で HttpClient を登録する例
MAUI では MauiProgram.cs でサービス登録できます。API キーの渡し方は後述しますが、ここでは「どこかから取得できる」前提で例を示します。
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => { });
// HttpClient を DI 管理(単純に 1 つ共有する最小構成)
builder.Services.AddSingleton(sp => new HttpClient
{
Timeout = TimeSpan.FromSeconds(60)
});
builder.Services.AddSingleton(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
// 実際は SecureStorage 等から取得する(後述)
var apiKey = "sk-xxxxxxxxxxxxx";
return new OpenAiChatService(httpClient, apiKey);
});
return builder.Build();
}
}
API キーをソースに直書きしない:Android でも安全に扱う設計
API キーをコードに埋め込むのは、デバッグ中は楽でも公開ビルドで非常に危険です。モバイルアプリは配布後に解析されやすく、キー流出は即コスト被害につながります。最低限、以下のどれかに寄せましょう。
| 方法 | 安全性 | 実装の手間 | おすすめ度 |
|---|---|---|---|
| SecureStorage(端末の安全領域) | 中 | 低 | 個人利用・検証向け。まずはこれ |
| 自前バックエンドでトークン発行(サーバー経由) | 高 | 中〜高 | 商用・チーム開発向け。流出耐性が高い |
| 設定ファイル(Git 管理外) | 低〜中 | 低 | ローカル開発では有効。配布アプリには不向き |
SecureStorage を使う場合は、初回起動時にキーを入力して保存し、以後は取り出して使う形にすると「コードに残さない」運用ができます。
using Microsoft.Maui.Storage;
public static class ApiKeyStore
{
private const string KeyName = "OPENAI_API_KEY";
public static Task SaveAsync(string apiKey)
=> SecureStorage.SetAsync(KeyName, apiKey);
public static async Task<string?> LoadAsync()
{
try
{
return await SecureStorage.GetAsync(KeyName);
}
catch
{
// 端末のロック設定などで SecureStorage が使えないケースもある
return null;
}
}
}
実務的な注意:SecureStorage は「端末の安全領域に保存する」仕組みであって、アプリ単体で完全に秘密を守れる魔法ではありません。商用アプリで不特定多数に配布するなら、API キー(あるいは課金に直結する権限)は基本的にサーバー側に置き、アプリは短命トークンで呼ぶ設計が安全です。
“Windows 前提のチュートリアル” が抱えがちな落とし穴
チュートリアルは学習効率を上げるために、環境差をなるべく無視して一直線に作れるように作られています。その結果、マルチプラットフォーム運用では次のような罠にハマりがちです。
- 依存パッケージのターゲットが Windows を含む:MAUI はマルチターゲットでも、追加した NuGet が実質 Windows 専用だと Android で破綻します。
- 設定/秘密情報の保存が Windows 的:UserSecrets や Windows 前提の保護機構を、そのまま Android に持ち込むと例外の温床になります。
- ファイルパスが固定:ローカルファイルへ書き込み・読み込みをする場合、Windows のパスや権限を前提にすると Android では動きません。
Android での再発防止:MAUI プロジェクトの“分離”を徹底する
同じ MAUI プロジェクトでも、意識して設計するとプラットフォーム例外は激減します。ポイントは「共有層にプラットフォーム依存を持ち込まない」ことです。
おすすめの分離パターン
- UI(Pages):MAUI コントロールだけで完結させる
- ViewModel / UseCase:純 .NET(ビジネスロジック)に寄せる
- 外部 API 呼び出し:インターフェース化して差し替え可能にする
- プラットフォーム固有:
Platforms/Androidなどに閉じ込める(必要なら partial class)
どうしても「Windows ではライブラリを使いたいが Android では直叩きしたい」という場合は、条件コンパイルで切り替えるのも現場的には有効です。
public interface IChatClient
{
Task<string> AskAsync(string message, CancellationToken ct = default);
}
public sealed class ChatClient : IChatClient
{
#if ANDROID
private readonly OpenAiChatService _service;
public ChatClient(OpenAiChatService service) => _service = service;
public Task AskAsync(string message, CancellationToken ct = default)
=> _service.AskAsync(message, ct);
#else
// ここに Windows でのみ動くクライアント実装を置く(ライブラリ利用など)
public Task AskAsync(string message, CancellationToken ct = default)
=> throw new NotImplementedException();
#endif
}
失敗しやすいポイントと対策チェックリスト
PlatformNotSupportedException が直った後も、Android 実機では別の“運用系エラー”が出ることがあります。リリース前に以下を一通り確認すると安定します。
| チェック項目 | 症状 | 対策 |
|---|---|---|
| ネットワーク権限(INTERNET) | 接続できずタイムアウト、DNS エラー | AndroidManifest の権限を確認(テンプレートでも念のため) |
| タイムアウト設定 | 電波が弱い環境で永遠に待つ | HttpClient.Timeout を設定し、UI でキャンセル可能に |
| 429(Rate limit) | 混雑時・連打でエラー | リトライ(指数バックオフ)、入力連打防止、キャッシュ |
| 401(認証) | API キー不正、権限不足 | キー取得フローを見直し。ログにステータスと本文を残す |
| 例外ログの取り方 | 再現できず詰む | App Center / Sentry 等で例外と環境情報を収集(個人情報は除外) |
よくある質問
ライブラリが Android 対応かどうかは、どこを見ればいい?
まずは NuGet の対応ターゲット(TFM)を見ます。net8.0-android や net7.0-android が明記されている、または netstandard2.0 のみで OS 依存 API を使っていない説明があると安心材料になります。逆に net8.0-windows しかない場合は、Android では基本的に動きません。
“ビルドは通るのに実行で落ちる” のはなぜ?
マルチターゲットの条件分岐や、実行時にだけ呼ばれるコードパスがあるためです。たとえば「起動後に設定を読み込む」「初回だけキーを保護する」などはビルド時に検出されにくく、Android でその処理が走った瞬間に PlatformNotSupportedException になります。
HttpClient 直叩きは将来的に不利?
短期的には実装量が増えますが、長期的に見ても「HTTP の基本を押さえた実装」は資産になります。特にモバイルはライブラリ都合で詰まることがあるため、“いつでも戻れる最低限の直叩き実装”を持っておくと保守が楽です。後から対応済みライブラリに切り替える場合も、インターフェースで包んでおけば影響は最小化できます。
まとめ:原因は MAUI ではなく “Android 未対応の依存” を踏んでいる可能性が高い
Android での System.PlatformNotSupportedException は、コードの書き方というより「その場で呼んだ API が Android では未サポート」という事実の表れです。Windows 前提チュートリアルをベースにした場合、OpenAI 向けの .NET ラッパーを含む依存関係が Android を想定していないことがあります。
確実に前進するためには、(1) スタックトレースで原因箇所を特定し、(2) ライブラリの対応状況を確認し、(3) 当面は HttpClient で REST API を直接呼ぶ方式に切り替える――この順番が最短です。共有層にプラットフォーム非依存の実装を置けば、Android/Windows の両方で同じコードを動かせるようになり、今後の拡張(ストリーミング、会話履歴、キャッシュ)もスムーズになります。

コメント