将 winapp CLI 与 Flutter 配合使用

有关完整的工作示例,请查看此存储库中的 Flutter 示例

本指南演示如何将 winapp CLI 与 Flutter 应用程序配合使用,以 MSIX 的形式添加包标识和打包应用。

包标识是Windows app模型中的核心概念。 它允许应用程序访问特定的Windows API(例如通知、安全、AI API 等),具有干净的安装/卸载体验等。

标准 Flutter Windows 生成没有包标识。 本指南演示如何添加它进行调试,然后将其打包以供分发。

先决条件

  1. Flutter SDK:按照 官方指南安装 Flutter。

  2. winapp CLI:通过 winget 安装或更新winapp CLI(如果已安装)。

    winget install Microsoft.winappcli --source winget
    

1. 创建新的 Flutter 应用

按照官方 Flutter 文档中的指南创建新的应用程序并运行它。

您应该能看到默认的 Flutter 计数器应用。

2. 更新代码以检查标识

我们将更新应用,以检查它是否使用包标识运行。 我们将使用 Dart FFI 调用 Windows GetCurrentPackageFamilyName API。

首先,添加 ffi 包:

flutter pub add ffi

接下来,用以下代码替换 lib/main.dart 的内容。 此代码尝试使用 Windows API 检索当前包标识。 如果成功,则会在 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.在没有标识的情况下运行

现在,像往常一样生成并运行应用:

flutter build windows

直接运行可执行文件(如果项目名称不同,请替换为 flutter_app):

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

小窍门

无论计算机的体系结构如何,生成输出都位于 x64 文件夹中,这应该适用于 Flutter 的Windows生成。

你应该会看到带有橙色“未打包”指示器的应用程序。 这确认标准可执行文件在没有任何包标识的情况下正在运行。

4.使用 winapp CLI 初始化Project

winapp init 命令一次性设置好所需的一切:应用清单、资产和可用于 C++ 开发的(可选)Windows 应用 SDK 标头。 清单定义应用的标识(名称、发布者、版本),Windows用于授予 API 访问权限。

运行以下命令并按照提示操作:

winapp init

出现提示时:

  • 包名称:按 Enter 接受默认值(派生自项目名称)
  • 发布者名称:按 Enter 接受默认值或输入名称
  • 版本:按 Enter 接受 1.0.0.0
  • Description:按 Enter 接受默认值(Windows应用程序)
  • 设置 SDKs:选择“稳定 SDKs”以下载 Windows 应用 SDK 并生成 C++ 标头(第 6 步需要)

此命令将:

  • 创建 Package.appxmanifest — 定义您应用身份的清单
  • 创建 Assets 文件夹 - MSIX 打包和应用商店提交所需的图标
  • 使用Windows 应用 SDK标头和库创建 .winapp 文件夹
  • 创建一个 winapp.yaml 配置文件用于锁定 SDK 版本

可以打开 Package.appxmanifest 以进一步自定义属性,如显示名称、发布者和功能。

使用身份进行调试

若要测试需要标识(如通知)且未完全打包应用的功能,可以使用 winapp run。 这会注册松散布局包(就像真正的 MSIX 安装一样),并在一个步骤中启动应用。 调试不需要证书或签名。

  1. 生成应用

    flutter build windows
    
  2. 使用标识运行

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

小窍门

winapp run 还会在您的系统中注册该软件包。 这就是为什么在步骤 7 的后面尝试安装 MSIX 时,MSIX 可能显示为“已安装”。 使用 winapp unregister 清理开发包以完成工作。

现在应会看到显示绿色指示器的应用:

Package Family Name: flutterapp.debug_xxxxxxxx

这确认你的应用正在运行,并具有有效的包标识!

小窍门

有关高级调试工作流(附加调试器、IDE 设置、启动调试),请参阅 调试指南

6. 使用 Windows 应用 SDK (可选)

如果选择在 winapp init 期间设置 SDK,则现在可以访问 .winapp/include 文件夹中的 Windows 应用 SDK C++ 标头。 由于 Flutter 的Windows运行程序是 C++,因此可以从本机代码调用 Windows 应用 SDK API,并通过方法通道将其公开给 Dart。 如果您只需要分发包标识,可以跳到步骤 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 以进行三项更改。 查找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"
)

然后在文件末尾添加这两行以链接 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 语句添加到顶部,与文件中的其他 include 语句一起。

#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 显示运行时版本。 在子列表中添加此小组件,该小组件 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.dlldata 文件夹,所有这些都是必需的。

生成开发证书

在打包之前,您需要一个用于签名的开发证书。 如果尚未生成一个:

winapp cert generate --if-exists skip

签名和打包

现在你可以打包并签名:

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

注意:此命令 pack 会自动使用当前目录中的 Package.appxmanifest 命令,并在打包之前将其复制到目标文件夹。

安装证书

在安装 MSIX 包之前,需要信任计算机上的开发证书。 以管理员身份运行此命令(每个证书只需执行此操作一次):

winapp cert install .\devcert.pfx

安装和运行

小窍门

如果在步骤 5 中使用 winapp run ,则可能已在系统上注册包。 首先使用 winapp unregister 删除开发注册,然后安装发布包。

双击生成的 .msix 文件或使用 PowerShell 安装包:

Add-AppxPackage .\flutterapp.msix

小窍门

MSIX 文件名包括版本和体系结构(例如)。 flutterapplication1_1.0.0.0_x64.msix 检查目录的确切文件名。 如果需要在代码更改后重新打包,请在 Version 中递增 Package.appxmanifest — Windows需要更高的版本号来更新已安装的包。

提示

  1. 准备好分发后,可以使用证书颁发机构的代码签名证书对 MSIX 进行签名,以便用户无需安装自签名证书。
  2. Azure 受信任签名 服务是安全地管理证书并将登录集成到 CI/CD 管道的好方法。
  3. Microsoft Store将为你签名 MSIX,无需在提交之前进行签名。

后续步骤

  • 通过 winget 分发:将 MSIX 提交到 Windows 程序包管理器 社区存储库
  • 发布到 Microsoft Store:使用 winapp store 提交包
  • 设置 CI/CD:使用 GitHub Action 在流水线中自动打包
  • 探索 Windows APIs:使用包标识,现在可以使用通知设备端 AI和其他身份依赖的 API