Menggunakan winapp CLI dengan Flutter

Untuk contoh kerja lengkap, lihat sampel Flutter di repositori ini.

Panduan ini menunjukkan cara menggunakan winapp CLI dengan aplikasi Flutter untuk menambahkan identitas paket dan mengemas aplikasi Anda sebagai MSIX.

Identitas paket adalah konsep inti dalam model Windows app. Ini memungkinkan aplikasi Anda untuk mengakses API Windows tertentu (seperti Notifikasi, Keamanan, API AI, dll), memiliki pengalaman penginstalan/penghapusan instalasi yang bersih, dan banyak lagi.

Build standar Flutter untuk Windows tidak memiliki identitas paket. Panduan ini menunjukkan cara menambahkannya untuk penelusuran kesalahan lalu mengemasnya untuk didistribusikan.

Prasyarat

  1. Flutter SDK: Instal Flutter mengikuti panduan resmi.

  2. winapp CLI: Instal winapp CLI melalui winget (atau perbarui jika sudah diinstal):

    winget install Microsoft.winappcli --source winget
    

1. Buat Aplikasi Flutter Baru

Ikuti panduan di dokumen Flutter resmi untuk membuat aplikasi baru dan menjalankannya.

Anda seharusnya melihat aplikasi penghitung Flutter bawaan.

2. Perbarui Kode untuk Memeriksa Identitas

Kami akan memperbarui aplikasi untuk memeriksa apakah aplikasi berjalan dengan identitas paket. Kami akan menggunakan Dart FFI untuk memanggil API Windows GetCurrentPackageFamilyName.

Pertama, tambahkan ffi paket:

flutter pub add ffi

Selanjutnya, ganti konten lib/main.dart dengan kode berikut. Kode ini mencoba mengambil identitas paket saat ini menggunakan API Windows. Jika berhasil, maka akan menampilkan Nama Keluarga Paket di UI; jika tidak, ini menunjukkan "Tidak dibungkus".

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. Jalankan Tanpa Identitas

Sekarang, bangun dan jalankan aplikasi seperti biasa:

flutter build windows

Jalankan executable secara langsung (ganti flutter_app dengan nama proyek Anda jika berbeda):

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

Tip

Hasil build berada di folder x64 tanpa memandang arsitektur komputer Anda — ini diharapkan untuk build Windows Flutter.

Anda akan melihat aplikasi dengan indikator oranye "Tidak dikemas". Ini mengonfirmasi bahwa executable standar berjalan tanpa identitas paket apa pun.

4. Inisialisasi Project dengan winapp CLI

Perintah winapp init menyiapkan semua yang Anda butuhkan dalam sekali jalan: manifes aplikasi, aset, dan secara opsional SDK Aplikasi Windows header untuk pengembangan C++. Manifes menentukan identitas aplikasi Anda (nama, penerbit, versi) yang Windows gunakan untuk memberikan akses API.

Jalankan perintah berikut dan ikuti perintah:

winapp init

Ketika diminta:

  • Nama paket: Tekan Enter untuk menerima default (berasal dari nama proyek Anda)
  • Nama penerbit: Tekan Enter untuk menerima pengaturan standar atau masukkan nama Anda
  • Versi: Tekan Enter untuk menerima 1.0.0.0
  • Description: Tekan Enter untuk menerima default (Aplikasi Windows)
  • Setup SDK: Pilih "SDK Stabil" untuk mengunduh SDK Aplikasi Windows dan menghasilkan header C++ (diperlukan untuk langkah 6)

Perintah ini akan:

  • Buat Package.appxmanifest — manifes yang menentukan identitas aplikasi Anda
  • Buat Assets folder — ikon yang diperlukan untuk pengemasan MSIX dan pengiriman Store
  • Buat folder .winapp dengan header dan pustaka SDK Aplikasi Windows
  • Membuat winapp.yaml file konfigurasi untuk menyematkan versi SDK

Anda dapat membuka Package.appxmanifest untuk menyesuaikan properti lebih lanjut seperti nama tampilan, penerbit, dan kemampuan.

5. Debugging dengan Identifikasi

Untuk menguji fitur yang memerlukan identitas (seperti Pemberitahuan) tanpa sepenuhnya mengemas aplikasi, Anda dapat menggunakan winapp run. Ini mendaftarkan paket tata letak tidak terikat (sama seperti penginstalan MSIX nyata) dan memulai aplikasi dalam satu langkah. Tidak diperlukan sertifikat atau penandatanganan untuk debug.

  1. Buat aplikasi:

    flutter build windows
    
  2. Jalankan dengan identitas:

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

Tip

winapp run juga mendaftarkan paket pada sistem Anda. Inilah sebabnya mengapa MSIX mungkin muncul sebagai "sudah diinstal" ketika Anda mencoba menginstalnya nanti di langkah 7. Gunakan winapp unregister untuk membersihkan paket pengembangan setelah selesai.

Anda sekarang akan melihat aplikasi dengan indikator hijau yang menunjukkan:

Package Family Name: flutterapp.debug_xxxxxxxx

Ini mengonfirmasi bahwa aplikasi Anda berjalan dengan identitas paket yang valid!

Tip

Untuk alur kerja penelusuran kesalahan tingkat lanjut (melampirkan debugger, penyiapan IDE, penelusuran kesalahan startup), lihat Panduan Penelusuran Kesalahan.

6. Menggunakan SDK Aplikasi Windows (Opsional)

Jika Anda memilih untuk mengatur SDK selama winapp init, Anda sekarang memiliki akses ke header C++ SDK Aplikasi Windows di folder .winapp/include. Karena runner Windows Flutter adalah C++, Anda dapat memanggil API SDK Aplikasi Windows dari kode asli dan mengeksposnya ke Dart melalui saluran metode. Jika Anda hanya memerlukan identitas paket untuk distribusi, Anda dapat melompat ke langkah 7.

Mari kita tambahkan contoh sederhana yang menampilkan versi runtime Windows App.

Membuat Plugin Asli

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

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

Memperbarui CMakeLists.txt

Edit windows/runner/CMakeLists.txt untuk membuat tiga perubahan. add_executable Temukan blok dan tambahkan "winapp_sdk_plugin.cpp" ke daftar file sumber:

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

Kemudian tambahkan dua baris ini di akhir file untuk menautkan pustaka WinRT dan sertakan header SDK Aplikasi 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")

Daftarkan Plugin

Di windows/runner/flutter_window.cpp, tambahkan include di bagian atas file bersama dengan include lainnya:

#include "winapp_sdk_plugin.h"

Kemudian temukan RegisterPlugins panggilan masuk FlutterWindow::OnCreate() dan tambahkan RegisterWinAppSdkPlugin di baris tepat setelah itu:

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

Memperbarui main.dart

Tambahkan impor berikut di bagian lib/main.dartatas , bersama dengan impor yang ada:

import 'package:flutter/services.dart';

Tambahkan fungsi ini di bawah fungsi yang ada getPackageFamilyName() (di luar kelas apa pun):

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

Di dalam kelas _MyHomePageState, tambahkan bidang baru di sebelah _packageFamilyName yang sudah ada.

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

Perbarui initState() untuk memanggil fungsi baru:

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

Terakhir, tampilkan versi runtime di dalam metode build. Tambahkan widget ini dalam daftar anak Column, tepat setelah Container yang menunjukkan identitas paket.

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

Kompilasi dan Jalankan

Membangun kembali aplikasi:

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

Anda sekarang akan melihat output seperti:

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

Direktori .winapp/include berisi semua header yang diperlukan untuk SDK Aplikasi Windows, termasuk:

  • winrt/ - Header proyeksi WinRT C++ untuk akses ke API Windows Runtime
  • Microsoft.UI.*.h - Header WinUI 3 untuk komponen UI modern
  • MddBootstrap.h - Pemulaian SDK Aplikasi Windows
  • WindowsAppSDK-VersionInfo.h - Informasi versi
  • Dan banyak lagi komponen SDK Aplikasi Windows

Untuk penggunaan SDK Aplikasi Windows yang lebih canggih, lihat dokumentasi SDK Aplikasi Windows.

7. Paket dengan MSIX

Setelah siap mendistribusikan aplikasi, Anda dapat mengemasnya sebagai MSIX menggunakan manifes yang sama.

Menyiapkan Direktori Paket

Pertama, buat aplikasi Anda dalam mode rilis:

flutter build windows

Kemudian, buat direktori dengan file rilis Anda:

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

Output build Flutter Windows mencakup file executable, flutter_windows.dll, dan folder data — yang semuanya diperlukan.

Membuat Sertifikat Pengembangan

Sebelum pengemasan, Anda memerlukan sertifikat pengembangan untuk penandatanganan. Hasilkan satu jika Anda belum melakukannya.

winapp cert generate --if-exists skip

Tanda tangani dan Kemas

Sekarang Anda dapat mengemas dan menandatangani:

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

Catatan: Perintah pack secara otomatis menggunakan Package.appxmanifest pada direktori saat ini Anda dan menyalinnya ke folder target sebelum pengemasan.

Menginstal Sertifikat

Sebelum Anda dapat menginstal paket MSIX, Anda perlu mempercayai sertifikat pengembangan pada komputer Anda. Jalankan perintah ini sebagai administrator (Anda hanya perlu melakukan ini sekali per sertifikat):

winapp cert install .\devcert.pfx

Instal dan Jalankan

Tip

Jika Anda menggunakan winapp run di langkah 5, paket mungkin sudah terdaftar di sistem Anda. Gunakan winapp unregister terlebih dahulu untuk menghapus pendaftaran pengembangan, lalu instal paket rilis.

Instal paket dengan mengeklik dua kali file yang dihasilkan .msix , atau menggunakan PowerShell:

Add-AppxPackage .\flutterapp.msix

Tip

Nama file MSIX mencakup versi dan arsitektur (misalnya, flutterapplication1_1.0.0.0_x64.msix). Periksa direktori Anda untuk nama file yang tepat. Jika Anda perlu mengemas ulang setelah perubahan kode, tingkatkan Version di Package.appxmanifest — Windows memerlukan nomor versi yang lebih tinggi untuk memperbarui paket yang diinstal.

Tips

  1. Setelah siap untuk didistribusikan, Anda dapat menandatangani MSIX dengan sertifikat penandatanganan kode dari Otoritas Sertifikat sehingga pengguna Anda tidak perlu menginstal sertifikat yang ditandatangani sendiri.
  2. Layanan Penandatanganan Tepercaya Azure adalah cara yang bagus untuk mengelola sertifikat Anda dengan aman dan mengintegrasikan penandatanganan ke dalam alur CI/CD Anda.
  3. Microsoft Store akan menandatangani MSIX untuk Anda, tidak perlu menandatangani sebelum pengiriman.

Langkah Selanjutnya