Siklus Hidup Aplikasi
Memahami siklus hidup aplikasi Wails dari mulai dijalankan hingga dihentikan
Memahami Siklus Hidup Aplikasi
Aplikasi desktop memiliki siklus hidup dari mulai dijalankan hingga dihentikan. Wails v3 menyediakan layanan, peristiwa, dan hook untuk mengelola siklus hidup ini secara efektif.
Tahapan Siklus Hidup
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:
- Opsi diuraikan dan divalidasi
- Layanan didaftarkan (tetapi belum dimulai)
- Server aset dikonfigurasi
- Runtime disiapkan
2. Menjalankan Aplikasi
Panggil app.Run() untuk memulai aplikasi:
err := app.Run() // Blocks until quit
if err != nil {
log.Fatal(err)
}Yang terjadi:
- Layanan dimulai sesuai urutan pendaftaran
- Listener peristiwa diaktifkan
- Jendela dapat dibuat
- Perulangan peristiwa dimulai
3. Perulangan Peristiwa
Aplikasi memasuki perulangan peristiwa, tempat aplikasi menghabiskan sebagian besar waktunya:
- Peristiwa OS diproses (peristiwa mouse, papan ketik, dan jendela)
- Pesan dari Go ke JS ditangani
- Panggilan dari JS ke Go dijalankan
- Pembaruan UI dirender
4. Penghentian
Saat aplikasi dihentikan:
- Callback
ShouldQuitdiperiksa (jika ditetapkan) - Callback
OnShutdowndijalankan - Layanan dihentikan dalam urutan terbalik
- Jendela ditutup
- Sumber daya dilepaskan
Siklus Hidup Layanan
Layanan merupakan cara utama untuk mengelola siklus hidup di Wails v3. Layanan menyediakan hook yang dipanggil saat proses mulai dan penghentian melalui antarmuka. Untuk dokumentasi lengkap tentang layanan, lihat panduan Layanan.
Membuat Layanan
type MyService struct {
db *sql.DB
}
// ServiceStartup is called when the application starts
func (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 down
func (s *MyService) ServiceShutdown() error {
if s.db != nil {
return s.db.Close()
}
return nil
}Mendaftarkan Layanan
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&MyService{}),
application.NewService(&AnotherService{}),
},
})Poin utama:
- Layanan dimulai sesuai urutan pendaftaran
- Layanan dihentikan dalam urutan pendaftaran terbalik
- Jika
ServiceStartupmilik suatu layanan mengembalikan kesalahan, aplikasi dibatalkan ctxyang diteruskan keServiceStartupdibatalkan saat proses penghentian dimulai
Menggunakan Konteks Aplikasi
Konteks yang diteruskan ke ServiceStartup berlaku selama masa aktif 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 konteks dari instans aplikasi:
app := application.Get()
ctx := app.Context()Hook Tingkat Aplikasi
Callback praktis dalam application.Options ini memungkinkan Anda mengaitkan kode ke siklus hidup aplikasi tanpa membuat layanan lengkap. Callback ini berguna untuk tugas pembersihan sederhana, konfirmasi keluar, atau saat Anda perlu menjalankan kode pada titik tertentu dalam urutan penonaktifan.
Untuk pengelolaan siklus hidup yang lebih kompleks dengan logika pengaktifan, injeksi dependensi, atau sumber daya berstatus, gunakan Layanan sebagai gantinya.
ShouldQuit
Callback ShouldQuit dipanggil setiap kali ada permintaan keluar—baik karena pengguna menutup jendela terakhir, menekan Cmd+Q (macOS) / Alt+F4 (Windows), maupun memanggil app.Quit() secara terprogram.
Nilai kembalian:
- Kembalikan
trueuntuk mengizinkan proses keluar dilanjutkan (aplikasi akan dinonaktifkan) - Kembalikan
falseuntuk membatalkan proses keluar (aplikasi tetap berjalan)
Pada tahap ini, Anda dapat mencegat dan, bila diperlukan, membatalkan permintaan keluar, misalnya untuk memberi tahu 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 ditetapkan, aplikasi akan langsung keluar saat diminta.
Kapan ShouldQuit dipanggil:
- Pengguna menutup jendela terakhir (kecuali jika
DisableQuitOnLastWindowClosedditetapkan) - Pengguna menekan Cmd+Q di macOS
- Pengguna menekan Alt+F4 di Windows (saat fokus berada pada jendela terakhir)
- Kode memanggil
app.Quit()
Kapan ShouldQuit TIDAK dipanggil:
- Proses dihentikan secara paksa (SIGKILL, penghentian paksa melalui Task Manager)
os.Exit()dipanggil secara langsung
OnShutdown
Callback OnShutdown dipanggil setelah dipastikan bahwa aplikasi akan keluar (setelah ShouldQuit mengembalikan true, jika ditetapkan). Gunakan callback ini untuk tugas pembersihan seperti menyimpan status, menutup koneksi basis data, atau melepaskan sumber daya.
app := application.New(application.Options{
OnShutdown: func() {
// Save application state
saveState()
// Close connections
cleanup()
},
})Anda juga dapat mendaftarkan callback penonaktifan tambahan secara terprogram kapan saja selama masa aktif aplikasi:
app.OnShutdown(func() {
log.Println("Application shutting down...")
})Beberapa callback dijalankan sesuai urutan pendaftarannya. Proses penonaktifan akan terblokir hingga semua callback selesai.
Penting: Pastikan callback penonaktifan berjalan cepat (kurang dari 1 detik). Sistem operasi dapat menghentikan paksa aplikasi yang memerlukan waktu terlalu lama untuk keluar, sehingga dapat menginterupsi proses pembersihan dan menyebabkan kehilangan data.
PostShutdown
Callback PostShutdown dipanggil setelah semua tugas penonaktifan selesai, tepat sebelum proses berakhir. Pada tahap ini, instans aplikasi tidak dapat digunakan lagi—semua jendela telah ditutup, layanan telah dinonaktifkan, dan sumber daya telah dilepaskan.
Callback ini terutama berguna untuk:
- Pencatatan log akhir yang harus dilakukan setelah semua pembersihan lainnya
- Pengujian dan penelusuran kesalahan perilaku penonaktifan
- Platform tempat
app.Run()tidak mengembalikan kontrol (callback memastikan kode Anda dijalankan)
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 (jendela, dialog, dan sebagainya) dalam PostShutdown—fitur tersebut sudah tidak tersedia.
Siklus Hidup Berbasis Peristiwa
Wails menyediakan sistem peristiwa yang memberi tahu Anda saat sesuatu terjadi dalam aplikasi—jendela dibuka, aplikasi dimulai, tema berubah, dan lainnya. Anda dapat memantau peristiwa ini untuk merespons perubahan siklus hidup tanpa memblokir atau mencegatnya.
Untuk peristiwa jendela, Anda juga dapat menggunakan RegisterHook sebagai pengganti OnWindowEvent guna mencegat dan membatalkan tindakan—misalnya, mencegah jendela ditutup. Lihat Hook Jendela di bawah ini.
Untuk dokumentasi lengkap tentang sistem peristiwa, lihat panduan Peristiwa.
Peristiwa Aplikasi
Pantau peristiwa siklus hidup aplikasi:
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
app.Logger.Info("Application has started!")
})Peristiwa khusus platform juga tersedia:
// macOS
app.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
})
// Windows
app.Event.OnApplicationEvent(events.Windows.ApplicationStarted, func(event *application.ApplicationEvent) {
// Handle Windows start
})Peristiwa Jendela
Pantau peristiwa siklus hidup jendela:
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")
})Hook Jendela (Peristiwa yang Dapat Dibatalkan)
Gunakan RegisterHook sebagai pengganti OnWindowEvent saat Anda perlu membatalkan suatu peristiwa:
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 saat suatu peristiwa terjadi (tidak dapat dibatalkan)RegisterHook: Memungkinkan Anda mencegat dan, bila diperlukan, membatalkan peristiwa
Siklus Hidup Jendela
Jendela memiliki siklus hidupnya sendiri, mulai dari pembuatan hingga penghancuran. Setiap jendela memuat konten frontend-nya secara independen dan dapat ditampilkan, disembunyikan, atau ditutup kapan saja. Saat pengguna mencoba menutup jendela, Anda dapat mencegat tindakan ini dengan RegisterHook untuk meminta konfirmasi atau menyembunyikan jendela alih-alih menghancurkannya.
Untuk dokumentasi lengkap tentang jendela, lihat panduan Jendela.
Membuat Jendela
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Window",
Width: 800,
Height: 600,
})Mencegah Jendela Ditutup
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()
})Menyembunyikan Alih-alih Menutup
Pola yang umum untuk aplikasi baki sistem:
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
window.Hide() // Hide instead of destroy
e.Cancel() // Prevent actual close
})Siklus Hidup Multi-Jendela
Dengan beberapa jendela:
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 berbeda-beda menurut platform:
| Platform | Perilaku default saat jendela terakhir ditutup |
|---|---|
| macOS | Aplikasi tetap berjalan (bilah menu tetap ada) |
| Windows | Aplikasi berhenti |
| Linux | Aplikasi berhenti |
macOS mengikuti konvensi bawaan platform, yaitu aplikasi biasanya tetap aktif di bilah menu meskipun tidak ada jendela. Secara default, aplikasi berhenti di Windows dan Linux.
Buat aplikasi berhenti di semua platform saat jendela terakhir ditutup:
app := application.New(application.Options{
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})Buat aplikasi tetap berjalan di semua platform saat jendela terakhir ditutup:
Ini berguna untuk aplikasi baki sistem atau aplikasi yang sebaiknya tetap berjalan di latar belakang.
app := application.New(application.Options{
Windows: application.WindowsOptions{
DisableQuitOnLastWindowClosed: true,
},
Linux: application.LinuxOptions{
DisableQuitOnLastWindowClosed: true,
},
})Pola Umum
Pola 1: Layanan Basis Data
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 frontend
func (s *DatabaseService) GetUsers() ([]User, error) {
// Query implementation
}Pola 2: Layanan Konfigurasi
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: Pekerja Latar Belakang
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
| Hook/Antarmuka | Waktu Dipanggil | Dapat Membatalkan? | Kegunaan |
|---|---|---|---|
ServiceStartup |
Selama app.Run(), sebelum perulangan peristiwa |
Tidak (kembalikan galat untuk membatalkan) | Inisialisasi |
ServiceShutdown |
Selama proses penghentian, setelah OnShutdown |
Tidak | Pembersihan |
OnShutdown |
Saat keluar dikonfirmasi | Tidak | Pembersihan aplikasi |
ShouldQuit |
Saat keluar diminta | Ya (kembalikan false) | Konfirmasi keluar |
RegisterHook(WindowClosing) |
Saat penutupan jendela diminta | Ya (e.Cancel()) |
Mencegah jendela ditutup |
OnWindowEvent |
Saat peristiwa terjadi | Tidak | Menanggapi peristiwa |
OnApplicationEvent |
Saat peristiwa terjadi | Tidak | Menanggapi peristiwa |
Perbedaan Platform
macOS
- Menu aplikasi tetap tersedia meskipun tidak ada jendela
- Cmd+Q memicu aplikasi untuk berhenti (melalui
ShouldQuit) - Ikon Dock tetap ditampilkan kecuali disembunyikan
- Gunakan
ApplicationShouldTerminateAfterLastWindowCloseduntuk mengendalikan perilaku saat aplikasi berhenti
Windows
- Tidak ada menu aplikasi tanpa jendela
- Alt+F4 menutup jendela (dapat dicegah dengan
RegisterHook) - Baki sistem dapat membuat aplikasi tetap berjalan
Linux
- Perilaku bervariasi menurut lingkungan desktop
- Secara umum mirip dengan Windows
Men-debug Masalah Siklus Hidup
Masalah: Aplikasi Tidak Dapat Berhenti
Penyebab:
ShouldQuitmengembalikanfalseOnShutdownmemerlukan waktu terlalu lama- Goroutine latar belakang tidak berhenti
Solusi:
// 1. Check ShouldQuit logic
ShouldQuit: func() bool {
log.Println("ShouldQuit called")
return true
}
// 2. Keep OnShutdown fast
OnShutdown: func() {
log.Println("OnShutdown started")
// Fast cleanup only
log.Println("OnShutdown finished")
}
// 3. Use context for background tasks
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
go func() {
<-ctx.Done()
log.Println("Context cancelled, stopping background work")
}()
return nil
}Masalah: Proses Memulai Layanan Gagal
Solusi: Kembalikan pesan kesalahan 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
}Kesalahan akan dicatat dan aplikasi tidak akan dimulai.
Praktik Terbaik
Lakukan
- Gunakan layanan untuk mengelola siklus hidup — Layanan menyediakan hook yang tepat untuk memulai dan menghentikan aplikasi
- Pastikan proses penghentian berlangsung cepat — Targetkan kurang dari 1 detik untuk seluruh pembersihan
- Gunakan context untuk pembatalan — Hentikan tugas latar belakang dengan benar
- Tangani kesalahan saat memulai aplikasi — Kembalikan kesalahan agar proses dapat dibatalkan dengan aman
- Catat peristiwa siklus hidup — Hal ini membantu proses debugging
Jangan Lakukan
- Jangan lakukan operasi yang memblokir saat layanan dimulai — Pastikan inisialisasi berlangsung cepat (kurang dari 2 detik)
- Jangan tampilkan dialog saat aplikasi dihentikan — Aplikasi sedang berhenti sehingga UI mungkin tidak berfungsi
- Jangan abaikan context — Selalu periksa
ctx.Done()dalam goroutine - Jangan biarkan sumber daya bocor — Selalu implementasikan
ServiceShutdown
Langkah Berikutnya
Layanan — Pelajari lebih lanjut tentang sistem layanan Pelajari Selengkapnya →
Sistem Peristiwa — Gunakan peristiwa untuk berkomunikasi Pelajari Selengkapnya →
Pengelolaan Jendela — Buat dan kelola jendela Pelajari Selengkapnya →
Ada pertanyaan tentang siklus hidup? Tanyakan di Discord atau lihat contoh.