Flutter での winapp CLI の使用

完全な実際の例については、このリポジトリの Flutter サンプル を確認してください。

このガイドでは、Flutter アプリケーションで winapp CLI を使用してパッケージ ID を追加し、アプリを MSIX としてパッケージ化する方法について説明します。

パッケージ ID は、Windows app モデルの主要な概念です。 これにより、アプリケーションは特定のWindows API (通知、セキュリティ、AI API など) にアクセスでき、クリーンなインストール/アンインストール エクスペリエンスが提供されます。

標準の Flutter Windows ビルドにはパッケージ ID がありません。 このガイドでは、デバッグ用に追加し、配布用にパッケージ化する方法を示します。

[前提条件]

  1. Flutter SDK: 公式ガイドに従って Flutter をインストールします。

  2. winapp CLI: winget を使用して winapp CLI をインストールします (既にインストールされている場合は更新します)。

    winget install Microsoft.winappcli --source winget
    

1. 新しい Flutter アプリを作成する

Flutter の公式ドキュメントのガイドに従って、新しいアプリケーションを作成して実行します。

既定の Flutter カウンター アプリが表示されます。

2. コードを更新して ID を確認する

アプリがパッケージ ID で実行されているかどうかを確認するように更新します。 Dart FFI を使用して、Windows GetCurrentPackageFamilyName API を呼び出します。

まず、 ffi パッケージを追加します。

flutter pub add ffi

次に、 lib/main.dart の内容を次のコードに置き換えます。 このコードは、Windows API を使用して現在のパッケージ ID の取得を試みます。 成功すると、UI にパッケージ ファミリ名が表示されます。それ以外の場合は、"パッケージ化されていません" と表示されます。

import 'dart:ffi';
import 'dart:io' show Platform;

import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';

/// Returns the Package Family Name if running with package identity, or null.
String? getPackageFamilyName() {
  if (!Platform.isWindows) return null;

  final kernel32 = DynamicLibrary.open('kernel32.dll');
  final getCurrentPackageFamilyName = kernel32.lookupFunction<
      Int32 Function(Pointer<Uint32>, Pointer<Uint16>),
      int Function(
          Pointer<Uint32>, Pointer<Uint16>)>('GetCurrentPackageFamilyName');

  final length = calloc<Uint32>();
  try {
    // First call to get required buffer length
    final result =
        getCurrentPackageFamilyName(length, Pointer<Uint16>.fromAddress(0));
    if (result != 122) return null; // ERROR_INSUFFICIENT_BUFFER = 122

    // Second call with buffer to get the name
    final namePtr = calloc<Uint16>(length.value);
    try {
      final result2 = getCurrentPackageFamilyName(length, namePtr);
      if (result2 == 0) {
        return namePtr.cast<Utf16>().toDartString(); // ERROR_SUCCESS = 0
      }
      return null;
    } finally {
      calloc.free(namePtr);
    }
  } finally {
    calloc.free(length);
  }
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  late final String? _packageFamilyName;

  @override
  void initState() {
    super.initState();
    _packageFamilyName = getPackageFamilyName();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              padding: const EdgeInsets.all(16),
              margin: const EdgeInsets.only(bottom: 24),
              decoration: BoxDecoration(
                color: _packageFamilyName != null
                    ? Colors.green.shade50
                    : Colors.orange.shade50,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(
                  color: _packageFamilyName != null
                      ? Colors.green
                      : Colors.orange,
                ),
              ),
              child: Text(
                _packageFamilyName != null
                    ? 'Package Family Name:\n$_packageFamilyName'
                    : 'Not packaged',
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodyLarge,
              ),
            ),
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

3. ID なしで実行する

次に、通常どおりにアプリをビルドして実行します。

flutter build windows

実行可能ファイルを直接実行します (異なる場合は、 flutter_app をプロジェクト名に置き換えます)。

.\build\windows\x64\runner\Release\flutter_app.exe

ヒント

ビルド出力は、コンピューターのアーキテクチャに関係なく、x64 フォルダーにあります。これは Flutter のWindows ビルドに必要です。

オレンジ色の "パッケージ化されていない" インジケーターが表示されたアプリが表示されます。 これにより、標準の実行可能ファイルがパッケージ ID なしで実行されていることが確認されます。

4. winapp CLI を使用してProjectを初期化する

winapp init コマンドは、アプリ マニフェスト、アセット、および必要に応じて C++ 開発用のWindows アプリ SDKヘッダーなど、必要なものをすべて一度に設定します。 マニフェストは、API アクセスを許可するために使用Windowsアプリの ID (名前、発行元、バージョン) を定義します。

次のコマンドを実行し、プロンプトに従います。

winapp init

プロンプトが表示されたら、次を実行します。

  • パッケージ名: Enter キーを押して既定値を受け入れます (プロジェクト名から派生)
  • Publisher名: Enter キーを押して既定値をそのまま使用するか、名前を入力します
  • バージョン: Enter キーを押して 1.0.0.0 を受け入れる
  • Description: Enter キーを押して既定値 (Windows アプリケーション) をそのまま使用します。
  • Setup SDK: "Stable SDK" を選択してWindows アプリ SDKをダウンロードし、C++ ヘッダーを生成します (手順 6 で必要)

このコマンドは次の操作を行います:

  • Package.appxmanifestの作成 - アプリの ID を定義するマニフェスト
  • Assets フォルダーの作成 - MSIX パッケージ化とストアの申請に必要なアイコン
  • Windows アプリ SDK ヘッダーとライブラリを含む .winapp フォルダーを作成する
  • SDK バージョンをピン留めするための winapp.yaml 構成ファイルを作成する

Package.appxmanifestを開いて、表示名、発行元、機能などのプロパティをさらにカスタマイズできます。

5. ID を使用したデバッグ

アプリを完全にパッケージ化せずに ID (通知など) を必要とする機能をテストするには、 winapp runを使用できます。 これにより、ルーズ レイアウト パッケージ (実際の MSIX インストールと同様) が登録され、1 つの手順でアプリが起動されます。 デバッグに証明書や署名は必要ありません。

  1. アプリをビルドします

    flutter build windows
    
  2. ID を使用して実行する:

    winapp run .\build\windows\x64\runner\Release
    

ヒント

winapp run は、パッケージをシステムに登録します。 このため、手順 7 で後で MSIX をインストールしようとすると、MSIX が "既にインストール済み" と表示される可能性があります。 完了したら、 winapp unregister を使用して開発パッケージをクリーンアップします。

緑色のインジケーターが表示されたアプリが表示されます。

Package Family Name: flutterapp.debug_xxxxxxxx

これにより、アプリが有効なパッケージ ID で実行されていることを確認できます。

ヒント

高度なデバッグ ワークフロー (デバッガーのアタッチ、IDE セットアップ、スタートアップ デバッグ) については、 デバッグ ガイドを参照してください。

6. Windows アプリ SDKの使用 (省略可能)

winapp init中に SDK をセットアップすることを選択した場合は、.winapp/include フォルダー内の Windows アプリ SDK C++ ヘッダーにアクセスできるようになりました。 Flutter のWindows ランナーは C++ であるため、ネイティブ コードから Windows アプリ SDK API を呼び出し、メソッド チャネルを介して Dart に公開できます。 配布にパッケージ ID が必要な場合は、手順 7 に進むことができます。

Windows アプリランタイムバージョンを表示する簡単な例を追加しましょう。

ネイティブ プラグインを作成する

windows/runner/winapp_sdk_plugin.hを作成します。

#ifndef RUNNER_WINAPP_SDK_PLUGIN_H_
#define RUNNER_WINAPP_SDK_PLUGIN_H_

#include <flutter/flutter_engine.h>

// Registers a method channel for querying Windows App SDK info.
void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine);

#endif  // RUNNER_WINAPP_SDK_PLUGIN_H_

windows/runner/winapp_sdk_plugin.cppを作成します。

#include "winapp_sdk_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <winrt/Microsoft.Windows.ApplicationModel.WindowsAppRuntime.h>

#include <string>

void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) {
  auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
      engine->messenger(), "com.example/winapp_sdk",
      &flutter::StandardMethodCodec::GetInstance());

  channel->SetMethodCallHandler(
      [](const flutter::MethodCall<flutter::EncodableValue>& call,
         std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
        if (call.method_name() == "getRuntimeVersion") {
          try {
            // Flutter already initializes COM in main.cpp, so we skip
            // winrt::init_apartment() here — the apartment is already set up.
            auto version = winrt::Microsoft::Windows::ApplicationModel::
                WindowsAppRuntime::RuntimeInfo::AsString();
            std::string versionStr = winrt::to_string(version);
            result->Success(flutter::EncodableValue(versionStr));
          } catch (const winrt::hresult_error& e) {
            result->Error("WINRT_ERROR", winrt::to_string(e.message()));
          } catch (...) {
            result->Error("UNKNOWN_ERROR",
                          "Failed to get Windows App Runtime version");
          }
        } else {
          result->NotImplemented();
        }
      });

  // prevent channel destruction by releasing ownership
  channel.release();
}

CMakeLists.txt の更新

windows/runner/CMakeLists.txtを編集して、3 つの変更を行います。 add_executable ブロックを見つけて、ソース ファイルの一覧に"winapp_sdk_plugin.cpp"を追加します。

add_executable(${BINARY_NAME} WIN32
  "flutter_window.cpp"
  "main.cpp"
  "utils.cpp"
  "win32_window.cpp"
  "winapp_sdk_plugin.cpp"       # <-- add this line
  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
  "Runner.rc"
  "runner.exe.manifest"
)

次に、ファイルの末尾に次の 2 行を追加して WinRT ライブラリをリンクし、Windows アプリ SDKヘッダーを含めます。

# Link Windows Runtime libraries for WinRT
target_link_libraries(${BINARY_NAME} PRIVATE "WindowsApp.lib")

# Windows App SDK headers from winapp CLI
target_include_directories(${BINARY_NAME} PRIVATE
  "${CMAKE_SOURCE_DIR}/../.winapp/include")

プラグインを登録する

windows/runner/flutter_window.cppで、ファイルの先頭に他のインクルードと共にインクルードを追加してください。

#include "winapp_sdk_plugin.h"

次に、RegisterPluginsFlutterWindow::OnCreate()呼び出しを見つけて、その直後の行にRegisterWinAppSdkPluginを追加します。

  RegisterPlugins(flutter_controller_->engine());
  RegisterWinAppSdkPlugin(flutter_controller_->engine());  // <-- add this line

main.dart を更新する

既存のインポートと共に、 lib/main.dartの上部に次のインポートを追加します。

import 'package:flutter/services.dart';

既存の getPackageFamilyName() 関数の下にこの関数を追加します (任意のクラスの外部)。

/// Queries the Windows App Runtime version via a native method channel.
Future<String?> getWindowsAppRuntimeVersion() async {
  if (!Platform.isWindows) return null;
  try {
    const channel = MethodChannel('com.example/winapp_sdk');
    final version = await channel.invokeMethod<String>('getRuntimeVersion');
    return version;
  } catch (_) {
    return null;
  }
}

_MyHomePageState クラスで、既存の_packageFamilyNameの横に新しいフィールドを追加します。

  late final String? _packageFamilyName;
  String? _runtimeVersion;         // <-- add this line

initState()を更新して新しい関数を呼び出します。

  @override
  void initState() {
    super.initState();
    _packageFamilyName = getPackageFamilyName();
    // Fetch the runtime version asynchronously
    getWindowsAppRuntimeVersion().then((version) {
      setState(() {
        _runtimeVersion = version;
      });
    });
  }

最後に、 build メソッドにランタイム バージョンを表示します。 パッケージ ID を示すColumnのすぐ後に、Containerの子リスト内に次のウィジェットを追加します。

            if (_runtimeVersion != null)
              Padding(
                padding: const EdgeInsets.only(bottom: 16),
                child: Text(
                  'Windows App Runtime: $_runtimeVersion',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
              ),

ビルドと実行

アプリケーションをリビルドします。

flutter build windows
winapp run .\build\windows\x64\runner\Release

次のような出力が表示されます。

Package Family Name: flutterapp.debug_xxxxxxxx
Windows App Runtime: 8000.731.1532.0

.winapp/include ディレクトリには、次のようなWindows アプリ SDKに必要なすべてのヘッダーが含まれています。

  • winrt/ - Windows ランタイム API にアクセスするための WinRT C++ プロジェクション ヘッダー
  • Microsoft.UI.*.h - 最新の UI コンポーネント用の WinUI 3 ヘッダー
  • MddBootstrap.h - Windows アプリ SDK のブートストラッピング
  • WindowsAppSDK-VersionInfo.h - バージョン情報
  • さらに多くのWindows アプリ SDKコンポーネント

Windows アプリ SDKの詳細については、Windows アプリ SDKドキュメントを参照してください。

7. MSIX を使用したパッケージ化

アプリを配布する準備ができたら、同じマニフェストを使用して MSIX としてパッケージ化できます。

パッケージ ディレクトリを準備する

まず、リリース モードでアプリケーションをビルドします。

flutter build windows

次に、リリース ファイルを含むディレクトリを作成します。

mkdir dist
copy .\build\windows\x64\runner\Release\* .\dist\ -Recurse

Flutter Windows ビルド出力には、実行可能ファイル、flutter_windows.dll、および data フォルダーが含まれます。これらはすべて必要です。

開発証明書を生成する

パッケージ化する前に、署名用の開発証明書が必要です。 まだ生成していない場合は生成します。

winapp cert generate --if-exists skip

署名とパッケージ

これで、パッケージ化と署名を行うことができます。

winapp pack .\dist --cert .\devcert.pfx

注: pack コマンドは、現在のディレクトリの Package.appxmanifest を自動的に使用し、パッケージ化する前にターゲット フォルダーにコピーします。

証明書をインストールする

MSIX パッケージをインストールする前に、コンピューター上の開発証明書を信頼する必要があります。 このコマンドを管理者として実行します (証明書ごとに 1 回だけ実行する必要があります)。

winapp cert install .\devcert.pfx

インストールと実行

ヒント

手順 5 で winapp run を使用した場合は、パッケージがシステムに既に登録されている可能性があります。 最初 winapp unregister 使用して開発登録を削除してから、リリース パッケージをインストールします。

生成された .msix ファイルをダブルクリックするか、PowerShell を使用してパッケージをインストールします。

Add-AppxPackage .\flutterapp.msix

ヒント

MSIX ファイル名には、バージョンとアーキテクチャ (例: flutterapplication1_1.0.0.0_x64.msix) が含まれます。 ディレクトリで正確なファイル名を確認します。 コードの変更後に再パッケージ化する必要がある場合は、VersionPackage.appxmanifest をインクリメントします。インストールされているパッケージを更新するには、Windows ではより大きなバージョン番号が必要です。

ヒント

  1. 配布の準備ができたら、証明機関のコード署名証明書を使用して MSIX に署名し、ユーザーが自己署名証明書をインストールする必要がないようにすることができます。
  2. Azure の信頼された署名 サービスは、証明書を安全に管理し、CI/CD パイプラインへの署名を統合するための優れた方法です。
  3. Microsoft Storeは MSIX に署名します。送信前に署名する必要はありません。

次のステップ

  • winget を使用して配布: MSIX を Windows パッケージ マネージャー Community Repository に送信します
  • Microsoft Store に発行する: を使用してパッケージを送信する
  • CI/CD の設定: setup-WinAppCli GitHub Action を使用して、パイプライン内のパッケージングを自動化します
  • Explore Windows API: パッケージ ID を使用して、 これで、Notificationson-device AI、およびその他の identity 依存 API