Использование интерфейса командной строки winapp с Flutter

Полный рабочий пример см. в примере Flutter в этом репозитории.

В этом руководстве показано, как использовать winapp CLI с приложением Flutter для идентификации пакета и создания пакета приложения в формате MSIX.

Идентификатор пакета — это основная концепция модели приложений Windows. Это позволяет приложению получать доступ к определенным API-интерфейсам Windows (например, уведомлениям, безопасности, API ИИ и т. д.), иметь чистый интерфейс установки и удаления и многое другое.

Стандартная сборка Flutter Windows не имеет идентификации пакета. В этом руководстве показано, как добавить его для отладки, а затем упаковать его для распространения.

Необходимые условия

  1. Пакет SDK для Flutter: установите Flutter после официального руководства.

  2. winapp CLI: Установите интерфейс командной строки с помощью winget (или обновите, если он уже установлен):

    winget install Microsoft.winappcli --source winget
    

1. Создание нового приложения Flutter

Следуйте инструкциям по официальному документу Flutter, чтобы создать новое приложение и запустить его.

Должно появиться приложение счетчика Flutter по умолчанию.

2. Обновление кода для проверки удостоверения

Мы обновим приложение, чтобы проверить, работает ли оно с идентификацией пакета. Мы будем использовать Dart FFI для вызова API Windows GetCurrentPackageFamilyName.

Сначала добавьте ffi пакет:

flutter pub add ffi

Затем замените содержимое lib/main.dart следующим кодом. Этот код пытается получить текущий идентификатор пакета с помощью API Windows. В случае успешного выполнения отображается имя семейства пакетов в пользовательском интерфейсе; в противном случае отображается сообщение "Не упаковано".

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

Команда winapp init настраивает все необходимое в одном шаге: манифест приложения, ресурсы и заголовки Windows App SDK для разработки на C++ при необходимости. Манифест определяет удостоверение приложения (имя, издатель, версия), которое Windows используется для предоставления доступа к API.

Выполните следующую команду и следуйте подсказкам.

winapp init

Когда появится запрос:

  • Название пакета: Нажмите Enter, чтобы принять значение по умолчанию (на основе имени проекта)
  • Имя издателя: нажмите клавишу Enter, чтобы принять значение по умолчанию или введите своё имя.
  • Версия: нажмите клавишу ВВОД, чтобы принять 1.0.0.0
  • Description: нажмите клавишу ВВОД, чтобы принять значение по умолчанию (приложение Windows)
  • Настройка SDKs: выберите "стабильные версии SDK", чтобы скачать Windows App SDK и сгенерировать заголовки C++ (необходимо для шага 6).

Эта команда выполнит следующее действие:

  • Создайте Package.appxmanifest — манифест, определяющий идентичность вашего приложения.
  • Создание Assets папки — значки, необходимые для упаковки MSIX и отправки в Магазин
  • Создание папки .winapp с заголовками и библиотеками Windows App SDK
  • Создайте файл конфигурации winapp.yaml для закрепления версий пакета SDK

Вы можете открыть Package.appxmanifest, чтобы дополнительно настроить такие свойства, как отображаемое имя, издатель и возможности.

5. Отладка с помощью идентификации

Для тестирования функций, требующих удостоверения (например, уведомлений) без полной упаковки приложения, можно использовать winapp run. Он регистрирует свободный пакет компоновки (как и реальную установку MSIX) и запускает приложение в один шаг. Для отладки не требуется сертификат или подпись.

  1. Создайте приложение:

    flutter build windows
    
  2. Запустите с идентификацией:

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

Подсказка

winapp run также регистрирует пакет в системе. Именно поэтому MSIX может отображаться как "уже установлен" при попытке установить его позже на шаге 7. Используйте winapp unregister для очистки пакетов разработки по завершении.

Теперь вы увидите приложение с зеленым индикатором:

Package Family Name: flutterapp.debug_xxxxxxxx

Это подтверждает, что ваше приложение работает с действительным удостоверением пакета!

Подсказка

Дополнительные рабочие процессы отладки (присоединение отладчиков, настройка интегрированной среды разработки, отладка запуска) см. в руководстве по отладке.

6. Использование Windows App SDK (необязательно)

Если вы выбрали настройку пакетов SDK во время winapp init, теперь у вас есть доступ к заголовкам Windows App SDK C++ в папке .winapp/include. Поскольку Windows runner Flutter написан на C++, вы можете вызывать API Windows App SDK из нативного кода и предоставлять их для 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 App 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 "winapp_sdk_plugin.h"

Затем найдите RegisterPlugins вызов в FlutterWindow::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 App SDK, включая:

  • winrt/ — заголовки проекции WinRT C++ для доступа к API среда выполнения Windows
  • Microsoft.UI.*.h — заголовки WinUI 3 для современных компонентов пользовательского интерфейса
  • MddBootstrap.h — инициализация Windows App SDK
  • WindowsAppSDK-VersionInfo.h — Сведения о версии
  • И многие другие Windows App SDK компоненты

Дополнительные сведения об использовании Windows App SDK см. в документации Windows App 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, необходимо доверять сертификату разработки на компьютере. Выполните эту команду от имени администратора (для каждого сертификата необходимо выполнить только один раз):

winapp cert install .\devcert.pfx

Установка и запуск

Подсказка

Если вы использовали winapp run на шаге 5, пакет уже может быть зарегистрирован в вашей системе. winapp unregister Сначала следует удалить регистрацию разработки, а затем установить финальный пакет.

Установите пакет, дважды щелкнув созданный .msix файл или используя PowerShell:

Add-AppxPackage .\flutterapp.msix

Подсказка

Имя файла MSIX включает версию и архитектуру (например, flutterapplication1_1.0.0.0_x64.msix). Проверьте каталог для точного имени файла. Если необходимо перепаковать после изменения кода, добавите Version в Package.appxmanifest — Windows требуется более высокий номер версии для обновления установленного пакета.

Tips

  1. После подготовки к распространению вы можете подписать MSIX с помощью сертификата подписи кода из центра сертификации, чтобы пользователи не должны устанавливать самозаверяющий сертификат.
  2. Служба Azure Trusted Signing — отличный способ безопасного управления сертификатами и интеграции входа в конвейер CI/CD.
  3. Microsoft Store подпишет MSIX за вас, так что нет необходимости подписывать его перед отправкой.

Дальнейшие шаги

  • Распространите с помощью winget: отправьте MSIX в репозиторий сообщества Windows диспетчер пакетов
  • Опубликовать в Microsoft Store: используйте winapp store для отправки пакета.
  • Set up CI/CD. Используйте действие setup-WinAppCli GitHub для автоматизации упаковки в конвейере.
  • Ознакомьтесь с Windows API: Теперь, с удостоверением пакета, вы можете использовать Уведомления, встроенный ИИ, и другие API-интерфейсы, зависящие от удостоверения