Uso dell'interfaccia della riga di comando di winapp con Flutter

Per un esempio funzionante completo, vedere l'esempio Flutter in questo repository.

Questa guida illustra come usare la CLI con un'applicazione Flutter per aggiungere l'identità del pacchetto e confezionare l'applicazione come MSIX.

L'identità del pacchetto è un concetto di base nel modello di Windows app. Consente all'applicazione di accedere a API di Windows specifiche (ad esempio Notifiche, Sicurezza, API di intelligenza artificiale e così via), avere un'esperienza di installazione/disinstallazione pulita e altro ancora.

Una compilazione standard di Flutter Windows non dispone dell'identità del pacchetto. Questa guida illustra come aggiungerlo per il debug e quindi crearne il pacchetto per la distribuzione.

Prerequisiti

  1. Flutter SDK: installare Flutter seguendo la guida ufficiale.

  2. winapp CLI: installare la winapp CLI tramite winget (o aggiornare se già installata):

    winget install Microsoft.winappcli --source winget
    

1. Creare una nuova app Flutter

Seguire la guida alla documentazione ufficiale di Flutter per creare una nuova applicazione ed eseguirla.

Verrà visualizzata l'app contatore Flutter predefinita.

2. Aggiornare il codice per controllare l'identità

Aggiorneremo l'app per verificare se è in esecuzione con l'identità del pacchetto. Utilizzeremo Dart FFI per chiamare le API Windows GetCurrentPackageFamilyName.

Aggiungere prima di tutto il ffi pacchetto:

flutter pub add ffi

Sostituire quindi il contenuto di lib/main.dart con il codice seguente. Questo codice tenta di recuperare l'identità del pacchetto corrente usando l'API Windows. Se ha esito positivo, visualizza il nome della famiglia di pacchetti nell'interfaccia utente; in caso contrario, viene visualizzato "Non incluso nel pacchetto".

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. Eseguire senza identità

A questo momento, compilare ed eseguire l'app come di consueto:

flutter build windows

Eseguire direttamente il file eseguibile (sostituire flutter_app con il nome del progetto, se diverso):

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

Suggerimento

L'output della compilazione si trova nella cartella x64 indipendentemente dall'architettura del computer. Questo è atteso per la build di Flutter su Windows.

Dovrebbe essere visualizzata l'app con un indicatore arancione "Non incluso nel pacchetto". Ciò conferma che l'eseguibile standard è in esecuzione senza alcuna identità del pacchetto.

4. Inizializzare il progetto con l'interfaccia della riga di comando di winapp

Il comando winapp init configura tutti gli elementi necessari in un unico passaggio: manifesto dell'app, asset e facoltativamente intestazioni SDK per app di Windows per lo sviluppo in C++. Il manifesto definisce l'identità dell'app (nome, editore, versione) che Windows usa per concedere l'accesso all'API.

Eseguire il comando seguente e seguire i prompt:

winapp init

Quando richiesto:

  • Nome pacchetto: premere INVIO per accettare il valore predefinito (derivato dal nome del progetto)
  • Publisher nome: premere INVIO per accettare il valore predefinito o immettere il nome
  • Versione: premere INVIO per accettare 1.0.0.0
  • Description: premere INVIO per accettare il valore predefinito (applicazione Windows)
  • Imposta SDK: Seleziona "Stable SDKs" per scaricare SDK per app di Windows e generare intestazioni C++ (necessarie per il passaggio 6)

Questo comando consentirà di:

  • Crea Package.appxmanifest : manifesto che definisce l'identità dell'app
  • Crea Assets cartella : icone necessarie per l'invio di pacchetti MSIX e dello Store
  • Creare una cartella .winapp con le intestazioni e le librerie delle SDK per app di Windows
  • Creare un winapp.yaml file di configurazione per bloccare le versioni dell'SDK

È possibile aprire Package.appxmanifest per personalizzare ulteriormente le proprietà, ad esempio il nome visualizzato, l'editore e le funzionalità.

5. Eseguire il debug con l'identità

Per testare le funzionalità che richiedono identità (ad esempio Notifiche) senza creare pacchetti completi dell'app, è possibile usare winapp run. Questo registra un pacchetto di layout libero (proprio come un'installazione MSIX reale) e avvia l'app in un unico passaggio. Non è necessario alcun certificato o firma per il debug.

  1. Compilare l'app:

    flutter build windows
    
  2. Esegui con privilegi di identità:

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

Suggerimento

winapp run registra anche il pacchetto nel sistema. Ecco perché MSIX può apparire come "già installato" quando si tenta di installarlo più avanti nel passaggio 7. Usare winapp unregister per pulire i pacchetti di sviluppo al termine.

L'app dovrebbe ora essere visualizzata con un indicatore verde che mostra:

Package Family Name: flutterapp.debug_xxxxxxxx

Ciò conferma che l'app è in esecuzione con un'identità del pacchetto valida.

Suggerimento

Per i flussi di lavoro di debug avanzati (collegamento di debugger, installazione dell'IDE, debug di avvio), vedere la Guida al debug.

6. Uso di SDK per app di Windows (facoltativo)

Se si è scelto di configurare gli SDK durante winapp init, è ora possibile accedere alle intestazioni SDK per app di Windows C++ nella cartella .winapp/include. Poiché il Windows runner di Flutter è C++, è possibile chiamare le API SDK per app di Windows dal codice nativo ed esporle a Dart tramite un canale di metodo. Se è sufficiente l'identità del pacchetto per la distribuzione, è possibile passare al passaggio 7.

Verrà ora aggiunto un semplice esempio che visualizza la versione di app di Windows runtime.

Creare il plug-in nativo

Creare 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_

Creare 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();
}

Aggiornare CMakeLists.txt

Modifica windows/runner/CMakeLists.txt per apportare tre modifiche. Trovare il add_executable blocco e aggiungere "winapp_sdk_plugin.cpp" all'elenco dei file di origine:

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

Aggiungere quindi queste due righe alla fine del file per collegare le librerie WinRT e includere le intestazioni SDK per app di 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")

Registrare il plug-in

In windows/runner/flutter_window.cpp aggiungere l'inclusione all'inizio del file con le altre inclusioni.

#include "winapp_sdk_plugin.h"

Trova quindi la RegisterPlugins chiamata in FlutterWindow::OnCreate() e aggiungi RegisterWinAppSdkPlugin nella riga immediatamente successiva:

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

Aggiorna main.dart

Aggiungere l'importazione seguente all'inizio di lib/main.dart, insieme alle importazioni esistenti:

import 'package:flutter/services.dart';

Aggiungere questa funzione sotto la funzione esistente getPackageFamilyName() (all'esterno di qualsiasi classe):

/// 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 Nella classe aggiungere un nuovo campo accanto all'oggetto esistente_packageFamilyName:

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

Aggiornare initState() per chiamare la nuova funzione:

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

Infine, visualizza la versione di runtime nel metodo build. Aggiungere questo widget all'interno dell'elenco degli Column elementi figli, subito dopo Container che mostra l'identità del pacchetto.

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

Compilare ed eseguire

Ricompilare l'applicazione:

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

Verrà visualizzato un output simile al seguente:

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

La directory .winapp/include contiene tutti i file di intestazione necessari per SDK per app di Windows, tra cui:

  • winrt/ - Intestazioni di proiezione C++ WinRT per l'accesso alle API di Windows Runtime
  • Microsoft.UI.*.h - Intestazioni WinUI 3 per i componenti moderni dell'interfaccia utente
  • MddBootstrap.h - Inizializzazione del SDK per app di Windows
  • WindowsAppSDK-VersionInfo.h - Informazioni sulla versione
  • E molti altri componenti SDK per app di Windows

Per un utilizzo SDK per app di Windows più avanzato, vedere la documentazione SDK per app di Windows.

7. Pacchetto con MSIX

Quando si è pronti per distribuire l'app, è possibile crearne il pacchetto come MSIX usando lo stesso manifesto.

Preparare la directory del pacchetto

Prima di tutto, compilare l'applicazione in modalità di rilascio:

flutter build windows

Creare quindi una directory con i file di rilascio:

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

L'output della compilazione Flutter Windows include l'eseguibile, flutter_windows.dll e una cartella data, tutte necessarie.

Generare un certificato di sviluppo

Prima dell'imballaggio, è necessario un certificato di sviluppo per apporre la firma. Generarne uno se non è già stato fatto:

winapp cert generate --if-exists skip

Firma e pacchetto

È ora possibile creare un pacchetto e firmare:

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

Nota: Il comando pack utilizza automaticamente il Package.appxmanifest dalla directory corrente e lo copia nella cartella di destinazione prima della creazione del pacchetto.

Installare il certificato

Prima di poter installare il pacchetto MSIX, è necessario considerare attendibile il certificato di sviluppo nel computer. Eseguire questo comando come amministratore (è necessario eseguire questa operazione una sola volta per ogni certificato):

winapp cert install .\devcert.pfx

Installare ed eseguire

Suggerimento

Se è stato usato winapp run nel passaggio 5, il pacchetto potrebbe essere già registrato nel sistema. Usare winapp unregister prima di tutto per rimuovere la registrazione di sviluppo, quindi installare il pacchetto di versione.

Installare il pacchetto facendo doppio clic sul file generato .msix o usando PowerShell:

Add-AppxPackage .\flutterapp.msix

Suggerimento

Il nome file MSIX include la versione e l'architettura (ad esempio, flutterapplication1_1.0.0.0_x64.msix). Controlla la directory per trovare il nome file esatto. Se è necessario creare un nuovo pacchetto dopo la modifica del codice, incrementare il Version nell'Package.appxmanifest: Windows richiede un numero di versione superiore per aggiornare un pacchetto installato.If you need to repackage after code changes, increment the Version in your Package.appxmanifest— Windows richiede un numero di versione superiore per aggiornare un pacchetto installato.

Tips

  1. Quando si è pronti per la distribuzione, è possibile firmare MSIX con un certificato di firma del codice da un'autorità di certificazione in modo che gli utenti non devono installare un certificato autofirmato.
  2. Il servizio Firma attendibile di Azure è un ottimo modo per gestire i certificati in modo sicuro e integrare l'accesso alla pipeline CI/CD.
  3. Il Microsoft Store firmerà MSIX per te, non è necessario firmare prima dell'invio.

Operazioni successive