Uso de la CLI de winapp con Flutter

Para obtener un ejemplo de trabajo completo, consulte el ejemplo de Flutter en este repositorio.

En esta guía se muestra cómo usar la winapp CLI con una aplicación flutter para agregar la identidad del paquete y empaquetar la aplicación como MSIX.

La identidad del paquete es un concepto básico en el modelo de Windows app. Permite a la aplicación acceder a api de Windows específicas (como notificaciones, seguridad, API de IA, etc.), tener una experiencia de instalación o desinstalación limpia, etc.

Una compilación estándar de Flutter Windows no tiene identidad de paquete. Esta guía muestra cómo agregarlo para depuración y luego empaquetarlo para su distribución.

Prerrequisitos

  1. SDK de Flutter: instale Flutter siguiendo la guía oficial.

  2. CLI de winapp: instale la winapp CLI mediante winget (o actualice si ya está instalada):

    winget install Microsoft.winappcli --source winget
    

1. Crear una nueva aplicación Flutter

Siga la guía de la documentación oficial de Flutter para crear una nueva aplicación y ejecutarla.

Deberías ver la aplicación predeterminada de contador de Flutter.

2. Actualizar código para comprobar la identidad

Actualizaremos la aplicación para comprobar si se ejecuta con la identidad del paquete. Usaremos Dart FFI para llamar a la API de Windows GetCurrentPackageFamilyName.

En primer lugar, agregue el ffi paquete:

flutter pub add ffi

A continuación, reemplace el contenido de lib/main.dart por el código siguiente. Este código intenta recuperar la identidad del paquete actual mediante la API de Windows. Si se ejecuta correctamente, muestra el nombre de familia del paquete en la interfaz de usuario; de lo contrario, muestra "No empaquetado".

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. Ejecutar sin identidad

Ahora, compile y ejecute la aplicación como de costumbre:

flutter build windows

Ejecute el archivo ejecutable directamente (reemplace por flutter_app el nombre del proyecto si es diferente):

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

Sugerencia

La salida de compilación se encuentra en la carpeta x64 independientemente de la arquitectura de la máquina, lo que se espera para la compilación de Windows de Flutter.

Debería ver la aplicación con un indicador naranja "No empaquetado". Esto confirma que el ejecutable estándar se está ejecutando sin ninguna identidad de paquete.

4. Inicializar Project con la CLI de winapp

El comando winapp init configura todo lo que necesita de una vez: manifiesto de la aplicación, recursos y, opcionalmente, encabezados de SDK de Aplicaciones para Windows para el desarrollo de C++. El manifiesto define la identidad de la aplicación (nombre, publicador, versión) que Windows usa para conceder acceso a la API.

Ejecute el siguiente comando y siga las indicaciones:

winapp init

Cuando se le solicite,

  • Nombre del paquete: presione Entrar para aceptar el valor predeterminado (derivado del nombre del proyecto)
  • Nombre del editor: Presione Intro para aceptar el valor predeterminado o escriba su nombre.
  • Versión: presione Entrar para aceptar 1.0.0.0.
  • Description: presione Entrar para aceptar el valor predeterminado (aplicación de Windows)
  • Setup SDK: seleccione "SDK estables" para descargar SDK de Aplicaciones para Windows y generar encabezados de C++ (necesarios para el paso 6)

Este comando hará lo siguiente:

  • Crear Package.appxmanifest : el manifiesto que define la identidad de la aplicación
  • Crear la carpeta Assets — iconos necesarios para el empaquetado MSIX y la publicación en la Tienda
  • Crear una carpeta .winapp con encabezados y bibliotecas del SDK de Aplicaciones para Windows
  • Creación de un winapp.yaml archivo de configuración para anclar versiones del SDK

Puede abrir Package.appxmanifest para personalizar aún más las propiedades, como el nombre para mostrar, el publicador y las funcionalidades.

5. Depurar con identidad

Para probar las características que requieren identidad (como notificaciones) sin empaquetar completamente la aplicación, puede usar winapp run. Esto registra un paquete de diseño flexible (al igual que una instalación MSIX real) e inicia la aplicación en un solo paso. No se necesita ningún certificado ni firma para la depuración.

  1. Compile la aplicación:

    flutter build windows
    
  2. Ejecute con identidad:

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

Sugerencia

winapp run también registra el paquete en el sistema. Este es el motivo por el que MSIX puede aparecer como "ya instalado" al intentar instalarlo más adelante en el paso 7. Use winapp unregister para limpiar los paquetes de desarrollo cuando haya terminado.

Ahora debería ver la aplicación con un indicador verde que muestra:

Package Family Name: flutterapp.debug_xxxxxxxx

Esto confirma que la aplicación se está ejecutando con una identidad de paquete válida.

Sugerencia

Para obtener flujos de trabajo de depuración avanzados (adjuntar depuradores, configuración del IDE, depuración de inicio), consulte la Guía de depuración.

6. Usar SDK de Aplicaciones para Windows (opcional)

Si ha seleccionado para configurar los SDK durante winapp init, ahora tiene acceso a los encabezados C++ del SDK de Aplicaciones para Windows en la carpeta .winapp/include. Dado que el ejecutable de Windows de Flutter está en C++, puedes llamar a las API de SDK de Aplicaciones para Windows desde código nativo y exponerlas a Dart a través de un canal de método. Si solo necesita la identidad del paquete para la distribución, puede ir al paso 7.

Vamos a agregar un ejemplo sencillo que muestra la versión de Aplicación de Windows Runtime.

Creación del complemento nativo

Creación de 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_

Creación de 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();
}

Actualizar CMakeLists.txt

Edite windows/runner/CMakeLists.txt para realizar tres cambios. Busque el add_executable bloque y agregue "winapp_sdk_plugin.cpp" a la lista de archivos de origen:

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"
)

A continuación, agregue estas dos líneas al final del archivo para vincular las bibliotecas de WinRT e incluya los encabezados SDK de Aplicaciones para Windows:

# 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")

Registro del complemento

En windows/runner/flutter_window.cpp, agregue el include en la parte superior del archivo junto con los otros includes.

#include "winapp_sdk_plugin.h"

A continuación, busque la RegisterPlugins llamada en FlutterWindow::OnCreate() y agregue RegisterWinAppSdkPlugin en la línea justo después de ella:

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

Actualizar main.dart

Agregue la siguiente importación en la parte superior de lib/main.dart, junto con las importaciones existentes:

import 'package:flutter/services.dart';

Agregue esta función debajo de la función existente getPackageFamilyName() (fuera de cualquier clase):

/// 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;
  }
}

En la _MyHomePageState clase , agregue un nuevo campo junto al existente _packageFamilyName:

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

Actualice initState() para llamar a la nueva función:

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

Por último, muestre la versión en tiempo de ejecución en el método build. Agregue este widget dentro de la Column lista de elementos secundarios, justo después de que Container muestre la identidad del paquete:

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

Compilación y ejecución

Reconstruya la aplicación:

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

Ahora debería ver un resultado similar a:

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

El directorio />

  • winrt/: encabezados de proyección de C++ de WinRT para acceder a las API de Windows Runtime
  • Microsoft.UI.*.h - Encabezados WinUI 3 para componentes de interfaz de usuario modernos
  • MddBootstrap.h: arranque de SDK de Aplicaciones para Windows
  • WindowsAppSDK-VersionInfo.h - Información de versión
  • Y muchos más componentes de SDK de Aplicaciones para Windows

Para obtener un uso de SDK de Aplicaciones para Windows más avanzado, consulte la documentación de SDK de Aplicaciones para Windows.

7. Paquete con MSIX

Una vez que esté listo para distribuir la aplicación, puede empaquetarla como MSIX mediante el mismo manifiesto.

Preparar el directorio de paquetes

En primer lugar, compile la aplicación en modo de versión:

flutter build windows

A continuación, cree un directorio con sus archivos de lanzamiento.

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

La salida de compilación de Flutter Windows incluye el archivo ejecutable, flutter_windows.dll, y una carpeta data — todo lo cual es necesario.

Generación de un certificado de desarrollo

Antes de empaquetar, necesita un certificado de desarrollo para firmar. Genere uno si aún no lo ha hecho:

winapp cert generate --if-exists skip

Firmar y empaquetar

Ahora puede empaquetar y firmar:

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

Nota: El comando pack usa automáticamente el Package.appxmanifest del directorio actual y lo copia en la carpeta de destino antes de paquetizar.

Instalación del certificado

Para poder instalar el paquete MSIX, debe confiar en el certificado de desarrollo en el equipo. Ejecute este comando como administrador (solo tiene que hacerlo una vez por certificado):

winapp cert install .\devcert.pfx

Instalación y ejecución

Sugerencia

Si usó winapp run en el paso 5, es posible que el paquete ya esté registrado en el sistema. Use winapp unregister primero para quitar el registro de desarrollo y, a continuación, instale el paquete de versión.

Instale el paquete haciendo doble clic en el archivo generado .msix o mediante PowerShell:

Add-AppxPackage .\flutterapp.msix

Sugerencia

El nombre de archivo MSIX incluye la versión y la arquitectura (por ejemplo, flutterapplication1_1.0.0.0_x64.msix). Compruebe el directorio para ver el nombre de archivo exacto. Si necesita volver a empaquetar después de que cambie el código, incremente el Version en la Package.appxmanifest: Windows requiere un número de versión superior para actualizar un paquete instalado.

Tips

  1. Una vez que esté listo para su distribución, puede firmar su MSIX con un certificado de firma de código de una Autoridad de Certificación para que los usuarios no tengan que instalar un certificado autofirmado.
  2. El servicio Firma de confianza de Azure es una excelente manera de administrar los certificados de forma segura e integrar el inicio de sesión en la canalización de CI/CD.
  3. El Microsoft Store firmará el MSIX por usted, no es necesario firmar antes del envío.

Pasos siguientes