Flutter와 함께 winapp CLI 사용

이 가이드에서는 Flutter 애플리케이션에서 winapp CLI를 사용하여 패키지 ID를 추가하고 앱을 MSIX로 패키지하는 방법을 보여 줍니다.

패키지 ID는 Windows app 모델의 핵심 개념입니다. 이를 통해 애플리케이션은 알림, 보안, AI API 등과 같은 특정 Windows API를 access, 깨끗한 설치/제거 환경 등을 사용할 수 있습니다.

필수 조건

  1. Flutter SDK: 공식 가이드에 따라 Flutter를 설치합니다.

  2. winapp CLI: winget을 사용하여 winapp CLI를 설치하십시오.

    winget install Microsoft.winappcli --source winget
    

1. 새 Flutter 앱 만들기

공식 Flutter 문서의 가이드에 따라 새 애플리케이션을 만들고 실행합니다.

2. ID를 확인하도록 코드 업데이트

ffi 패키지를 추가합니다.

flutter pub add ffi

lib/main.dart의 내용을 Windows GetCurrentPackageFamilyName API를 통해 다트 FFI를 사용하여 패키지 ID를 확인하는 다음 코드로 바꿉니다.

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

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

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 {
    final result =
        getCurrentPackageFamilyName(length, Pointer<Uint16>.fromAddress(0));
    if (result != 122) return null; // ERROR_INSUFFICIENT_BUFFER = 122

    final namePtr = calloc<Uint16>(length.value);
    try {
      final result2 = getCurrentPackageFamilyName(length, namePtr);
      if (result2 == 0) {
        return namePtr.cast<Utf16>().toDartString();
      }
      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
.\build\windows\x64\runner\Release\flutter_app.exe

주황색 "패키지되지 않음" 표시기가 있는 앱이 표시됩니다.

4. winapp CLI를 사용하여 project 초기화

winapp init

프롬프트가 표시되면

  • 패키지 이름: Enter 키를 눌러 기본값 적용
  • Publisher 이름: Enter 키를 눌러 기본값을 적용하거나 이름을 입력합니다.
  • 버전: Enter 키를 눌러 1.0.0.0 허용
  • 진입점: Enter 키를 눌러 기본값(flutter_app.exe)을 적용합니다.
  • SDK 설정: "안정적인 SDK"를 선택하여 Windows App SDK 다운로드하고 C++ 헤더를 생성합니다.

5. ID를 사용하여 디버그

  1. 앱을 빌드합니다.

    flutter build windows
    
  2. 디버그 ID 적용:

    winapp create-debug-identity .\build\windows\x64\runner\Release\flutter_app.exe
    
  3. 실행 파일을 실행합니다.

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

패키지 패밀리 이름을 표시하는 녹색 표시기가 있는 앱이 표시됩니다.

비고

실행 flutter clean 하거나 다시 빌드한 후에는 실행 파일이 대체되었으므로 다시 실행 create-debug-identity 해야 합니다.

6. MSIX를 사용하여 패키지

  1. 릴리스용 빌드:

    flutter build windows
    
  2. 패키지 디렉터리 준비:

    mkdir dist
    copy .\build\windows\x64\runner\Release\* .\dist\ -Recurse
    
  3. 개발 인증서 생성:

    winapp cert generate --if-exists skip
    
  4. 패키지 및 서명:

    winapp pack .\dist --cert .\devcert.pfx
    
  5. 인증서 설치 (관리자 권한으로 실행):

    winapp cert install .\devcert.pfx
    
  6. 패키지를 설치합니다.

    Add-AppxPackage .\flutter-app.msix
    

팁 (조언)

  • Microsoft Store에서 MSIX에 서명합니다. 제출하기 전에 서명할 필요가 없습니다.
  • Azure 신뢰할 수 있는 서명 CI/CD pipelines 대한 인증서를 안전하게 관리하는 좋은 방법입니다.