Menguasai fondasi GUI: event loop yang menjalankan aplikasi, konsep widget dan pohon widget, serta pengelolaan UI state, lalu mempraktikkannya dengan membangun aplikasi GUI dasar yang responsif dan tidak membeku

Setelah di episode 2 kita memahami arsitektur aplikasi desktop — pemisahan UI process dan backend process, threading model, dan IPC — kini saatnya memahami apa yang membuat antarmuka itu sendiri hidup. Setiap aplikasi desktop, sebesar apa pun, berjalan di atas tiga konsep yang sama: event loop, widget, dan UI state.
Menguasai ketiganya adalah pembeda antara aplikasi yang terasa licin dan aplikasi yang terasa kaku. Ketika kalian paham bagaimana event loop memproses klik, bagaimana widget dibangun dan dibangun ulang, dan bagaimana state mengalir, kalian bisa menulis GUI yang responsif tanpa menebak-nebak. Episode ini menjadi fondasi untuk tiga episode framework berikutnya (Tauri, Electron, Flutter) — konsep yang sama muncul di ketiganya dengan nama berbeda.
Aplikasi GUI tidak berjalan seperti script biasa (langkah 1 → 2 → 3 → selesai). Ia berjalan sebagai loop tak berujung yang menunggu peristiwa: klik mouse, tekanan keyboard, resize window, timer, atau data masuk dari IPC. Setiap peristiwa masuk ke antrean dan diproses satu per satu.
Konsekuensi penting: karena diproses satu per satu, satu handler yang lambat akan memblokir semua peristiwa berikutnya — itulah "freeze" yang kita bahas di episode 2. Handler harus secepat mungkin, atau pekerjaan berat dipindahkan ke thread lain.
Semua framework desktop memakai pola yang sama, meski dengan istilah berbeda:
while (aplikasi berjalan):
event = antrean.tunggu_peristiwa()
handler = cari_handler(event)
handler(event) # cepat!
render_ulang_jika_perlu()Di Electron/Tauri, loop ini milik webview (Blink/WebView). Di Flutter, milik engine Flutter. Di toolkit native seperti WinUI atau GTK, milik framework masing-masing. Yang penting: kalian tidak menulis loop-nya, tetapi kalian wajib memahami aturan mainnya — jangan pernah memblokirnya.
UI desktop disusun dari widget — komponen kecil yang bisa disusun seperti balok. Sebuah button, text field, list, dan layout adalah widget. Susunan widget membentuk pohon widget (widget tree):
Window
└─ Column (arah vertikal)
├─ Toolbar (Row: tombol baru, tombol simpan, pencarian)
├─ Expanded (daftar catatan)
└─ Editor (text area untuk isi catatan)Ketika state berubah, framework tidak menggambar ulang semuanya. Ia membandingkan pohon widget lama dan baru, lalu hanya memperbarui bagian yang berubah — inilah yang membuat UI tetap cepat meski punya ribuan widget.
function NoteList({ notes, onSelect }) {
return (
<ul>
{notes.map(n => <li key={n.id} onClick={() => onSelect(n)}>{n.title}</li>)}
</ul>
);
}ListView.builder(
itemCount: notes.length,
itemBuilder: (context, i) => ListTile(
title: Text(notes[i].title),
onTap: () => onSelect(notes[i]),
),
);Perhatikan persamaannya: keduanya adalah fungsi dari state — diberi data yang sama, menghasilkan tampilan yang sama. Ini prinsip UI modern yang akan terus kita pakai.
UI state adalah semua data yang menentukan tampilan: catatan yang terbuka, teks yang diketik, hasil pencarian, status sinkronisasi. Prinsip utamanya: satu sumber kebenaran (single source of truth) — state disimpan di satu tempat, dan tampilan selalu merupakan proyeksi dari state itu.
type AppState = {
notes: Note[];
activeNoteId: string | null;
searchQuery: string;
syncStatus: "idle" | "syncing" | "error";
};Perubahan UI mengikuti alur satu arah: peristiwa → ubah state → render ulang:
Event (klik simpan) → dispatch action → reducer mengubah state → render barufunction appReducer(state: AppState, action: Action): AppState {
switch (action.type) {
case "note/save":
return { ...state, notes: upsert(state.notes, action.note) };
case "search/set":
return { ...state, searchQuery: action.query };
default:
return state;
}
}Reducer yang murni (input sama → output sama, tanpa efek samping) membuat state mudah diprediksi dan diuji — fondasi yang akan kita pakai saat testing di episode 15.
Note
Istilah state di desktop dan web menunjuk hal yang sama, tetapi jangan tertukar dengan state di backend. State UI hidup di frontend dan hanya menyimpan "bentuk tampilan". Data persisten (catatan, settings) adalah urusan backend process dan storage — dibedah di episode 8.
Sebagai latihan pertama, kita bangun jendela CatatanKu yang menampilkan daftar catatan dan kotak pencarian — belum ada backend, cukup UI dan state. Kita pakai pendekatan framework-agnostik dengan TypeScript agar kalian bisa langsung menerjemahkannya ke Tauri (React) di episode 4 atau Electron di episode 5:
import { useReducer, useState } from "react";
const initialNotes = [
{ id: 1, title: "Belajar Tauri" },
{ id: 2, title: "Catatan rapat" },
];
function App() {
const [notes] = useState(initialNotes);
const [query, setQuery] = useState("");
const filtered = notes.filter(n =>
n.title.toLowerCase().includes(query.toLowerCase())
);
return (
<main style={{ padding: 16 }}>
<input
placeholder="Cari catatan..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ul>
{filtered.map((n) => <li key={n.id}>{n.title}</li>)}
</ul>
</main>
);
}
export default App;Perhatikan alurnya: input → setQuery → state berubah → filtered dihitung ulang → daftar dirender. Tidak ada manipulasi DOM manual — cukup ubah state, tampilan mengikuti. Inilah cara kerja GUI modern.
Inti yang harus dibawa pulang:
Di episode 4 selanjutnya kita akan membangun CatatanKu pertama yang nyata di Tauri 2 (Rust + Web) — dari scaffold project, commands, IPC, hingga plugin ecosystem. Pastikan tooling Rust sudah terinstall, karena kita mulai menulis kode framework pertama!