Membedah arsitektur aplikasi desktop: pemisahan UI process dan backend process, model rendering, threading model, dan peran IPC yang menjadi jembatan antarproses, lengkap dengan praktik merancang arsitektur untuk aplikasi CatatanKu

Setelah di episode 1 kita memahami peran desktop developer dan relevansinya di 2026, kini saatnya membedah apa yang terjadi di dalam sebuah aplikasi desktop. Sebuah aplikasi seperti CatatanKu — dengan editor, daftar catatan, pencarian, dan sinkronisasi — bukan satu blok kode monolitik yang berjalan di satu tempat.
Arsitektur yang baik menentukan segalanya: apakah aplikasi membeku saat menyimpan file besar, apakah crash di satu window meruntuhkan seluruh aplikasi, dan seberapa mudah kita menambahkan fitur baru. Episode ini membangun fondasi arsitektur yang akan dipakai di seluruh series — dan konsep-konsep ini berlaku sama di Tauri, Electron, maupun Flutter.
Aplikasi desktop modern hampir selalu membagi diri menjadi dua proses logis:
Mengapa dipisah? Karena UI harus selalu responsif. Bayangkan menekan Ctrl+S untuk menyimpan 2000 catatan — jika penyimpanan berjalan di thread UI, jendela akan membeku selama operasi. Dengan pemisahan ini, UI tetap bisa digambar sementara backend bekerja di belakang.
Model rendering membedakan framework desktop:
| Framework | Teknologi Render | Siapa yang Menggambar |
|---|---|---|
| Electron | Chromium (bundled) | WebView/Blink di dalam aplikasi |
| Tauri 2 | WebView sistem OS | WebView milik OS (WKWebView/WebView2/WebKitGTK) |
| Flutter | Engine Flutter + Skia/Impeller | Canvas native, bukan WebView |
Tauri tidak membundel browser karena ia memanfaatkan WebView yang sudah ada di sistem — ini penyebab ukuran biner-nya kecil. Flutter menggambar UI-nya sendiri ke kanvas, sehingga konsisten di semua platform. Ketiganya tetap memakai konsep UI process yang sama.
Setiap aplikasi desktop punya thread utama (main thread) yang menjalankan event loop UI. Aturannya sederhana tapi mutlak: jangan pernah menjalankan pekerjaan berat di thread utama. Pekerjaan yang berjalan lama di sana membuat UI "not responding" — tombol tidak bereaksi, kursor berputar, pengguna mencabut aplikasi.
# Di Electron: jangan lakukan ini di main/renderer thread
# while (true) { } → UI hang sampai proses di-killPola yang benar: pindahkan pekerjaan berat ke thread/worker terpisah, lalu kirim hasilnya kembali. Masing-masing framework menyediakan mekanismenya:
use tauri::async_runtime::spawn;
#[tauri::command]
async fn index_notes(path: String) -> Result<usize, String> {
spawn(async move { index_files(path).await }).await.map_err(|e| e.to_string())
}// main process
const { Worker } = require("node:worker_threads");
app.whenReady().then(() => {
const worker = new Worker("./index-worker.js");
worker.on("message", (count) => mainWindow.webContents.send("indexed", count));
});import 'dart:isolate';
Future<int> indexNotes() async {
return await Isolate.run(() => heavyIndexing());
}Ketiganya menjawab masalah yang sama: pekerjaan berat dijalankan di luar thread UI, hasilnya dikirim kembali via pesan. Detail pola async per framework dibedah di episode 9.
Jika UI process dan backend process adalah dua negara terpisah, IPC (Inter-Process Communication) adalah perjanjian lintas batas: aturan resmi tentang apa yang boleh dikirim, dalam format apa, dan apa yang boleh dijawab. Di Electron, IPC menghubungkan renderer dan main process; di Tauri, menghubungkan webview dan Rust; di Flutter, platform channels menghubungkan Dart dan native.
#[tauri::command]
fn create_note(title: String) -> Result<u32, String> {
Ok(save_note(title))
}
// dipanggil dari frontend: await invoke('create_note', { title })// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("desktop", {
createNote: (title) => ipcRenderer.invoke("notes:create", title),
});
// main process
ipcMain.handle("notes:create", (_event, title) => saveNote(title));const channel = MethodChannel("catatanku/native");
final id = await channel.invokeMethod<int>("createNote", {"title": title});Aturan emas IPC: permukaan komunikasi dibuat seminimal dan seketat mungkin. Setiap channel yang dibuka adalah permukaan serangan baru — kita akan mengeraskan ini di episode 14 dan 18.
Note
Istilah "main process" bisa membingungkan. Di Electron, main process adalah backend (Node.js) dan renderer adalah UI. Di Tauri, Rust side adalah backend dan webview adalah UI. Di Flutter, isolate utama adalah UI dan platform side adalah backend. Selalu perjelas konteksnya sebelum membaca kode.
Sebagai praktik, kita rancang arsitektur CatatanKu dengan prinsip di atas:
┌─────────────────────────────────────────────┐
│ UI Process (responsif, state lokal) │
│ ├─ Daftar catatan ├─ Editor │
│ └─ Pencarian └─ Settings │
├─────────────────────────────────────────────┤
│ IPC — permukaan API ketat: │
│ notes:list, notes:create, notes:update │
│ search:query, index:status, sync:run │
├─────────────────────────────────────────────┤
│ Backend Process │
│ ├─ Storage layer (SQLite) ├─ Indexer │
│ ├─ Sync engine └─ System APIs │
└─────────────────────────────────────────────┘Keputusan arsitektur kunci: UI tidak pernah menyentuh file langsung. Semua akses disk, database, dan sinkronisasi lewat IPC menuju backend. Ini membuat UI mudah diuji, backend mudah diganti, dan keamanan mudah diaudit — tiga keuntungan yang akan terus kita panen di episode-episode berikutnya.
Inti yang harus dibawa pulang:
Di episode 3 selanjutnya kita akan masuk ke GUI fundamentals — event loop, widget, dan UI state — dan membangun aplikasi GUI dasar pertama kalian. Pastikan tooling episode 0 sudah siap, karena kita mulai menulis kode nyata!