Siklus Hidup Aplikasi
Memahami Siklus Hidup Aplikasi
Section titled “Memahami Siklus Hidup Aplikasi”Aplikasi desktop memiliki siklus hidup dari startup hingga shutdown. Wails v3 menyediakan service, event, dan hook untuk mengelola siklus hidup ini secara efektif.
Tahap Siklus Hidup
Section titled “Tahap Siklus Hidup”1. Pembuatan Aplikasi
Section titled “1. Pembuatan Aplikasi”Buat aplikasi Anda dengan application.New():
app := application.New(application.Options{ Name: "My App", Description: "An application built with Wails", Services: []application.Service{ application.NewService(&MyService{}), }, Assets: application.AssetOptions{ Handler: application.BundledAssetFileServer(assets), },})Yang terjadi:
- Options di-parse dan divalidasi
- Service didaftarkan (tetapi belum dijalankan)
- Asset server dikonfigurasi
- Runtime disiapkan
2. Menjalankan Aplikasi
Section titled “2. Menjalankan Aplikasi”Panggil app.Run() untuk memulai aplikasi:
err := app.Run() // Blocks until quitif err != nil { log.Fatal(err)}Yang terjadi:
- Service dijalankan sesuai urutan registrasi
- Event listener diaktifkan
- Window dapat dibuat
- Event loop dimulai
3. Event Loop
Section titled “3. Event Loop”Aplikasi memasuki event loop di mana ia menghabiskan sebagian besar waktunya:
- Event OS diproses (mouse, keyboard, event window)
- Pesan Go-ke-JS ditangani
- Panggilan JS-ke-Go dieksekusi
- Pembaruan UI dirender
4. Shutdown
Section titled “4. Shutdown”Ketika aplikasi keluar:
- Callback
ShouldQuitdiperiksa (jika diset) - Callback
OnShutdowndieksekusi - Service di-shutdown dalam urutan terbalik
- Window ditutup
- Resource dilepas
Siklus Hidup Service
Section titled “Siklus Hidup Service”Service adalah cara utama mengelola siklus hidup di Wails v3. Mereka menyediakan hook startup dan shutdown melalui interface. Untuk dokumentasi lengkap tentang service, lihat Panduan Service.
Membuat Service
Section titled “Membuat Service”type MyService struct { db *sql.DB}
// ServiceStartup is called when the application startsfunc (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { var err error s.db, err = sql.Open("sqlite3", "app.db") if err != nil { return err // Startup aborts if error returned }
// Run migrations if err := s.runMigrations(); err != nil { return err }
return nil}
// ServiceShutdown is called when the application shuts downfunc (s *MyService) ServiceShutdown() error { if s.db != nil { return s.db.Close() } return nil}Mendaftarkan Service
Section titled “Mendaftarkan Service”app := application.New(application.Options{ Services: []application.Service{ application.NewService(&MyService{}), application.NewService(&AnotherService{}), },})Poin penting:
- Service dimulai sesuai urutan registrasi
- Service di-shutdown dalam urutan registrasi terbalik
- Jika
ServiceStartupservice mengembalikan error, aplikasi dibatalkan ctxyang diteruskan keServiceStartupdibatalkan saat shutdown dimulai
Menggunakan Application Context
Section titled “Menggunakan Application Context”Context yang diteruskan ke ServiceStartup valid selama masa hidup aplikasi:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { // Start a background task that respects shutdown go func() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop()
for { select { case <-ticker.C: s.performBackgroundSync() case <-ctx.Done(): // Application is shutting down return } } }()
return nil}Anda juga dapat mengakses context dari instance aplikasi:
app := application.Get()ctx := app.Context()Hook Tingkat Aplikasi
Section titled “Hook Tingkat Aplikasi”Ini adalah callback kemudahan di application.Options yang memungkinkan Anda terhubung ke siklus hidup aplikasi tanpa membuat service penuh. Berguna untuk tugas cleanup sederhana, konfirmasi quit, atau ketika Anda perlu menjalankan kode pada titik tertentu dalam urutan shutdown.
Untuk manajemen siklus hidup yang lebih kompleks dengan logika startup, dependency injection, atau resource stateful, gunakan Service.
ShouldQuit
Section titled “ShouldQuit”Callback ShouldQuit dipanggil setiap kali quit diminta—baik oleh pengguna menutup window terakhir, menekan Cmd+Q (macOS) / Alt+F4 (Windows), atau memanggil app.Quit() secara programatik.
Nilai return:
- Return
trueuntuk mengizinkan quit melanjutkan (aplikasi akan shutdown) - Return
falseuntuk membatalkan quit (aplikasi tetap berjalan)
Ini kesempatan Anda untuk mencegat permintaan quit dan opsional mencegahnya, misalnya untuk memprompt pengguna tentang perubahan yang belum disimpan:
app := application.New(application.Options{ ShouldQuit: func() bool { if !hasUnsavedChanges() { return true // No unsaved changes, allow quit }
// Prompt the user — MessageDialog.Show() blocks and returns nothing. // The button's OnClick callback fires for whichever button the user picks. shouldQuit := false
dlg := application.Get().Dialog.Question(). SetTitle("Unsaved Changes"). SetMessage("You have unsaved changes. Quit anyway?")
quit := dlg.AddButton("Quit") cancel := dlg.AddButton("Cancel") dlg.SetDefaultButton(cancel) dlg.SetCancelButton(cancel)
quit.OnClick(func() { shouldQuit = true })
dlg.Show() return shouldQuit },})Jika ShouldQuit tidak diset, aplikasi akan langsung quit saat diminta.
Kapan ShouldQuit dipanggil:
- Pengguna menutup window terakhir (kecuali
DisableQuitOnLastWindowCloseddiset) - Pengguna menekan Cmd+Q di macOS
- Pengguna menekan Alt+F4 di Windows (saat fokus pada window terakhir)
- Kode memanggil
app.Quit()
Kapan ShouldQuit TIDAK dipanggil:
- Proses di-kill (SIGKILL, force-quit Task Manager)
os.Exit()dipanggil langsung
OnShutdown
Section titled “OnShutdown”Callback OnShutdown dipanggil ketika aplikasi dikonfirmasi akan quit (setelah ShouldQuit return true, jika diset). Gunakan ini untuk tugas cleanup seperti menyimpan state, menutup koneksi database, atau melepaskan resource.
app := application.New(application.Options{ OnShutdown: func() { // Save application state saveState()
// Close connections cleanup() },})Anda juga dapat mendaftarkan callback shutdown tambahan secara programatik kapan saja selama siklus hidup aplikasi:
app.OnShutdown(func() { log.Println("Application shutting down...")})Beberapa callback dieksekusi sesuai urutan pendaftaran. Proses shutdown memblokir hingga semua callback selesai.
Penting: Jaga callback shutdown tetap cepat (di bawah 1 detik). Sistem operasi dapat memaksa terminasi aplikasi yang terlalu lama quit, yang dapat menginterupsi cleanup dan menyebabkan kehilangan data.
PostShutdown
Section titled “PostShutdown”Callback PostShutdown dipanggil setelah semua tugas shutdown selesai, tepat sebelum proses terminasi. Pada titik ini, instance aplikasi tidak lagi dapat digunakan—window ditutup, service di-shutdown, dan resource dilepas.
Ini terutama berguna untuk:
- Logging akhir yang harus terjadi setelah semua cleanup lain
- Testing dan debugging perilaku shutdown
- Platform di mana
app.Run()tidak return (callback memastikan kode Anda berjalan)
app := application.New(application.Options{ PostShutdown: func() { // Final logging log.Println("Application terminated cleanly")
// Flush any buffered logs logger.Sync() },})Catatan: Jangan mencoba menggunakan fitur aplikasi (window, dialog, dll.) di PostShutdown—fitur tersebut tidak lagi tersedia.
Siklus Hidup Berbasis Event
Section titled “Siklus Hidup Berbasis Event”Wails menyediakan sistem event yang memberi tahu Anda ketika sesuatu terjadi di aplikasi—window terbuka, aplikasi dimulai, perubahan tema, dan lainnya. Anda dapat mendengarkan event ini untuk bereaksi terhadap perubahan siklus hidup tanpa memblokir atau mencegatnya.
Untuk event window, Anda juga dapat menggunakan RegisterHook alih-alih OnWindowEvent untuk mencegat dan membatalkan aksi—misalnya, mencegah window ditutup. Lihat Window Hook di bawah.
Untuk dokumentasi lengkap tentang sistem event, lihat Panduan Event.
Event Aplikasi
Section titled “Event Aplikasi”Dengarkan event siklus hidup aplikasi:
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { app.Logger.Info("Application has started!")})Event spesifik platform juga tersedia:
// macOSapp.Event.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(event *application.ApplicationEvent) { // Handle macOS launch})
app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(event *application.ApplicationEvent) { // Handle macOS termination})
// Windowsapp.Event.OnApplicationEvent(events.Windows.ApplicationStarted, func(event *application.ApplicationEvent) { // Handle Windows start})Event Window
Section titled “Event Window”Dengarkan event siklus hidup window:
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Logger.Info("Window gained focus")})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { app.Logger.Info("Window is closing")})Window Hook (Event yang Dapat Dibatalkan)
Section titled “Window Hook (Event yang Dapat Dibatalkan)”Gunakan RegisterHook alih-alih OnWindowEvent ketika Anda perlu membatalkan event:
window := app.Window.New()
var countdown = 3
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { countdown-- if countdown > 0 { app.Logger.Info("Not closing yet!", "remaining", countdown) e.Cancel() // Prevent the window from closing return } app.Logger.Info("Window closing now")})Perbedaan antara OnWindowEvent dan RegisterHook:
OnWindowEvent: Memberi tahu Anda ketika event terjadi (tidak dapat dibatalkan)RegisterHook: Memungkinkan Anda mencegat dan potensial membatalkan event
Siklus Hidup Window
Section titled “Siklus Hidup Window”Window memiliki siklus hidup sendiri, dari pembuatan hingga penghancuran. Setiap window memuat konten frontend-nya secara independen dan dapat ditampilkan, disembunyikan, atau ditutup kapan saja. Ketika pengguna mencoba menutup window, Anda dapat mencegatnya dengan RegisterHook untuk memprompt konfirmasi atau menyembunyikan window alih-alih menghancurkannya.
Untuk dokumentasi window lengkap, lihat Panduan Window.
Membuat Window
Section titled “Membuat Window”window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "My Window", Width: 800, Height: 600,})Mencegah Penutupan Window
Section titled “Mencegah Penutupan Window”window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { if !hasUnsavedChanges() { return }
// MessageDialog.Show() returns nothing; per-button OnClick handlers fire. dlg := application.Get().Dialog.Question(). SetTitle("Unsaved Changes"). SetMessage("Save before closing?")
save := dlg.AddButton("Save") discard := dlg.AddButton("Discard") cancel := dlg.AddButton("Cancel") dlg.SetDefaultButton(save) dlg.SetCancelButton(cancel)
save.OnClick(func() { saveChanges() }) cancel.OnClick(func() { e.Cancel() }) // Prevent close _ = discard // "Discard" falls through and allows close
dlg.Show()})Sembunyikan Alih-alih Tutup
Section titled “Sembunyikan Alih-alih Tutup”Pola umum untuk aplikasi system tray:
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { window.Hide() // Hide instead of destroy e.Cancel() // Prevent actual close})Siklus Hidup Multi-Window
Section titled “Siklus Hidup Multi-Window”Dengan beberapa window:
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Main Window",})
settingsWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "Settings", Width: 400, Height: 600, Hidden: true, // Start hidden})Perilaku default bervariasi per platform:
| Platform | Default saat window terakhir ditutup |
|---|---|
| macOS | App tetap berjalan (menu bar tetap ada) |
| Windows | App quit |
| Linux | App quit |
macOS mengikuti konvensi platform native di mana aplikasi biasanya tetap aktif di menu bar meskipun tanpa window. Windows dan Linux quit secara default.
Buat semua platform quit saat window terakhir ditutup:
app := application.New(application.Options{ Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: true, },})Buat semua platform tetap berjalan saat window terakhir ditutup:
Ini berguna untuk aplikasi system tray atau app yang harus tetap berjalan di background.
app := application.New(application.Options{ Windows: application.WindowsOptions{ DisableQuitOnLastWindowClosed: true, }, Linux: application.LinuxOptions{ DisableQuitOnLastWindowClosed: true, },})Pola Umum
Section titled “Pola Umum”Pola 1: Database Service
Section titled “Pola 1: Database Service”type DatabaseService struct { db *sql.DB}
func (s *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { var err error s.db, err = sql.Open("sqlite3", "app.db") if err != nil { return fmt.Errorf("failed to open database: %w", err) }
if err := s.db.PingContext(ctx); err != nil { return fmt.Errorf("failed to connect to database: %w", err) }
return nil}
func (s *DatabaseService) ServiceShutdown() error { if s.db != nil { return s.db.Close() } return nil}
// Exported methods are available to the frontendfunc (s *DatabaseService) GetUsers() ([]User, error) { // Query implementation}Pola 2: Configuration Service
Section titled “Pola 2: Configuration Service”type ConfigService struct { config *Config path string}
func (s *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { s.path = "config.json"
data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { s.config = &Config{} // Default config return nil } return err }
return json.Unmarshal(data, &s.config)}
func (s *ConfigService) ServiceShutdown() error { data, err := json.MarshalIndent(s.config, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0644)}Pola 3: Background Worker
Section titled “Pola 3: Background Worker”type WorkerService struct { cancel context.CancelFunc}
func (s *WorkerService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { workerCtx, cancel := context.WithCancel(ctx) s.cancel = cancel
go s.runWorker(workerCtx)
return nil}
func (s *WorkerService) ServiceShutdown() error { if s.cancel != nil { s.cancel() } return nil}
func (s *WorkerService) runWorker(ctx context.Context) { ticker := time.NewTicker(time.Minute) defer ticker.Stop()
for { select { case <-ticker.C: s.doWork() case <-ctx.Done(): return } }}Referensi Siklus Hidup
Section titled “Referensi Siklus Hidup”| Hook/Interface | Kapan Dipanggil | Dapat Dibatalkan? | Digunakan Untuk |
|---|---|---|---|
ServiceStartup | Selama app.Run(), sebelum event loop | Tidak (return error untuk abort) | Inisialisasi |
ServiceShutdown | Selama shutdown, setelah OnShutdown | Tidak | Cleanup |
OnShutdown | Ketika quit dikonfirmasi | Tidak | Cleanup aplikasi |
ShouldQuit | Ketika quit diminta | Ya (return false) | Konfirmasi quit |
RegisterHook(WindowClosing) | Ketika penutupan window diminta | Ya (e.Cancel()) | Cegah penutupan window |
OnWindowEvent | Ketika event terjadi | Tidak | Bereaksi terhadap event |
OnApplicationEvent | Ketika event terjadi | Tidak | Bereaksi terhadap event |
Perbedaan Platform
Section titled “Perbedaan Platform”- Menu aplikasi tetap ada meskipun tanpa window
- Cmd+Q memicu quit (melalui
ShouldQuit) - Ikon Dock tetap kecuali disembunyikan
- Gunakan
ApplicationShouldTerminateAfterLastWindowCloseduntuk mengontrol perilaku quit
Windows
Section titled “Windows”- Tanpa menu aplikasi tanpa window
- Alt+F4 menutup window (dapat dicegah dengan
RegisterHook) - System tray dapat menjaga app tetap berjalan
- Perilaku bervariasi menurut desktop environment
- Umumnya mirip Windows
Debugging Masalah Siklus Hidup
Section titled “Debugging Masalah Siklus Hidup”Masalah: Aplikasi Tidak Mau Quit
Section titled “Masalah: Aplikasi Tidak Mau Quit”Penyebab:
ShouldQuitreturnfalseOnShutdownterlalu lama- Goroutine background tidak berhenti
Solusi:
// 1. Check ShouldQuit logicShouldQuit: func() bool { log.Println("ShouldQuit called") return true}
// 2. Keep OnShutdown fastOnShutdown: func() { log.Println("OnShutdown started") // Fast cleanup only log.Println("OnShutdown finished")}
// 3. Use context for background tasksfunc (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { go func() { <-ctx.Done() log.Println("Context cancelled, stopping background work") }() return nil}Masalah: Service Startup Gagal
Section titled “Masalah: Service Startup Gagal”Solusi: Return error yang deskriptif:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { if err := s.init(); err != nil { return fmt.Errorf("failed to initialise: %w", err) } return nil}Error akan dicatat dan aplikasi tidak akan start.
Praktik Terbaik
Section titled “Praktik Terbaik”Lakukan
Section titled “Lakukan”- Gunakan service untuk manajemen siklus hidup - Mereka menyediakan hook startup/shutdown yang tepat
- Jaga shutdown tetap cepat - Target di bawah 1 detik untuk semua cleanup
- Gunakan context untuk pembatalan - Hentikan tugas background dengan benar
- Tangani error di startup - Return error untuk abort dengan bersih
- Log event siklus hidup - Membantu debugging
Jangan
Section titled “Jangan”- Jangan block di service startup - Jaga inisialisasi tetap cepat (di bawah 2 detik)
- Jangan tampilkan dialog di shutdown - App sedang quit, UI mungkin tidak berfungsi
- Jangan abaikan context - Selalu periksa
ctx.Done()di goroutine - Jangan bocorkan resource - Selalu implementasikan
ServiceShutdown
Langkah Selanjutnya
Section titled “Langkah Selanjutnya”Service - Pelajari selengkapnya tentang sistem service Pelajari Selengkapnya →
Sistem Event - Gunakan event untuk komunikasi Pelajari Selengkapnya →
Manajemen Window - Buat dan kelola window Pelajari Selengkapnya →
Pertanyaan tentang siklus hidup? Tanyakan di Discord atau lihat contoh.