.NET MAUI Android の System.PlatformNotSupportedException を解決する方法|OpenAI/ChatGPT ライブラリ非対応時の回避策(HttpClient)

.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 / TypeLoadExceptionNuGet の対応 TFM を確認し、モバイル対応版に替える
暗号化/保護 API(例:Windows の DPAPI 相当)を内部で使用キー保護や資格情報保存でプラットフォーム固有 API を呼ぶPlatformNotSupportedException が OpenAI 呼び出し前後で発生モバイル向けの安全な保存(SecureStorage 等)に寄せる
Windows のファイルパスやレジストリなど OS 固有機能C:\ やレジストリ参照などAndroid で即死、または設定読み込みで例外プラットフォーム分離(条件コンパイル/Partial)
WinUI/WPF 前提の UI・依存 DLLUI 層に Windows 専用参照が混入起動時にクラッシュ共有層を “純 MAUI” に保つ

まずやるべき切り分け:例外メッセージより “スタックトレース” を見る

PlatformNotSupportedException はメッセージが抽象的なので、解決の近道は 例外が投げられた「最初の自分のコード行」を特定することです。Visual Studio の例外表示でスタックトレースを展開し、以下を確認します。

  • 例外が投げられた直前に呼んでいるメソッド名(ライブラリ側の型/名前空間)
  • 自分のプロジェクトのファイル名と行番号(ここが起点)
  • どの NuGet から呼ばれているか(パッケージ名が出ることも多い)

ここで OpenAI ラッパーの内部(例:認証ヘッダー生成、設定読み込み、ログ出力、ストリーミング処理など)に落ちているなら、ライブラリが Android で未対応、もしくは Android 用の実装が欠けている可能性が高いです。

依存パッケージを“犯人捜し”する具体的な手順

原因の NuGet が 1 つとは限りません。特にラッパー系は「依存の依存(トランジティブ依存)」が多く、そこに Windows 専用が混ざっていることもあります。次の順に潰すと早いです。

  1. 例外発生箇所を特定:スタックトレースで自分のコード行まで戻す
  2. その周辺で呼んでいるライブラリを洗い出す:AI クライアント、設定読み込み、保存処理など
  3. パッケージ一覧を確認:直接参照だけでなくトランジティブも含めて見る
  4. 一度パッケージを外して起動確認:外して動くなら依存が原因と確定しやすい

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 の両方で同じコードを動かせるようになり、今後の拡張(ストリーミング、会話履歴、キャッシュ)もスムーズになります。

この記事を書いた人

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

コメント

コメントする

目次