Membangun CatatanKu di Flutter Desktop: satu codebase Dart untuk desktop dan mobile, sistem widget dengan hot reload, serta platform channels untuk memanggil API native lintas OS

Kita sudah membangun CatatanKu dua kali: di Tauri (Rust + WebView) di episode 4 dan Electron (Node.js + Chromium) di episode 5. Sekarang pendekatan ketiga: Flutter Desktop. Berbeda dari keduanya, Flutter tidak memakai teknologi web sama sekali — UI digambar langsung ke kanvas oleh engine Flutter (Skia/Impeller) dari kode Dart.
Keunggulan unik Flutter: satu codebase untuk desktop dan mobile. Jika kalian juga membangun aplikasi Android/iOS, Flutter Desktop memungkinkan UI yang konsisten di lima platform sekaligus. Sama seperti Tauri dan Electron, Flutter punya konsep jembatan ke sistem — disebut platform channels — yang kita pakai untuk memanggil API native.
Karena Flutter menggambar sendiri, aplikasi tidak bergantung pada WebView atau browser — hasilnya konsisten dan cepat, namun ada biaya: ukuran biner dan memori lebih besar dari Tauri.
flutter create catatanku_flutter
cd catatanku_flutter
flutter run -d linux # ganti: windows / macosStruktur yang dihasilkan sudah mengelompokkan kode per platform:
catatanku_flutter/
├─ lib/
│ ├─ main.dart # entry point aplikasi
│ └─ ... # widgets, state, models
├─ linux/ # embedder Linux (GTK)
├─ macos/ # embedder macOS
├─ windows/ # embedder Windows
└─ pubspec.yamlflutter run -d linux membuka jendela dengan counter demo. Mari kita ganti dengan UI CatatanKu yang sebenarnya.
Flutter adalah Flutter karena sistem widget-nya. Semuanya widget — bahkan layout dan padding. UI CatatanKu dibangun dengan menyusun widget:
import 'package:flutter/material.dart';
void main() => runApp(const CatatanKuApp());
class CatatanKuApp extends StatelessWidget {
const CatatanKuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CatatanKu',
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final List<String> _notes = [];
final _controller = TextEditingController();
void _addNote() {
setState(() => _notes.add(_controller.text));
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CatatanKu')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(controller: _controller, hintText: 'Judul catatan'),
),
Expanded(
child: ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, i) => ListTile(title: Text(_notes[i])),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addNote,
child: const Icon(Icons.add),
),
);
}
}Perhatikan setState(() => _notes.add(...)) — ini cara Flutter menandakan "state berubah, render ulang". Ini analog dengan setQuery di episode 3: ubah state, tampilan mengikuti. StatefulWidget memegang state yang bisa berubah; StatelessWidget murni dari input.
Salah satu fitur favorit Flutter: hot reload. Saat aplikasi berjalan, tekan r di terminal untuk memuat ulang UI tanpa kehilangan state, atau R untuk hot restart. Proses iterasi desain jadi jauh lebih cepat dibanding membangun ulang biner native setiap perubahan.
Tip
Flutter memiliki sistem theming terintegrasi (ThemeData, ColorScheme.fromSeed). Kita akan memakai kekuatannya penuh di episode 10 saat membahas UI & theming desktop — untuk sekarang, cukup kenali bahwa MaterialApp menerima theme: dengan satu objek konfigurasi.
Widget Flutter tidak bisa langsung berbicara dengan sistem — untuk memanggil API native (file dialog, keychain, window management), kita pakai platform channels. Ini analog dengan invoke Tauri dan ipcRenderer.invoke Electron.
import 'package:flutter/services.dart';
class NativeBridge {
static const _channel = MethodChannel('catatanku/native');
static Future<String> pickFile() async {
final path = await _channel.invokeMethod<String>('pickFile');
return path ?? '';
}
static Future<bool> writeFile(String path, String content) async {
return await _channel.invokeMethod<bool>('writeFile', {
'path': path,
'content': content,
}) ?? false;
}
}Sisi native ditulis per platform dalam bahasa masing-masing: C++ untuk Windows/Linux, Swift untuk macOS. Contoh implementasi Windows:
// Set MethodChannel untuk "catatanku/native"
const auto channel = flutter::MethodChannel<flutter::EncodableValue>(
flutter_controller()->engine().messenger(), "catatanku/native");
channel.SetMethodCallHandler(
[](const auto& call, auto result) {
if (call.method_name() == "pickFile") {
// panggil Win32 API GetOpenFileName
result->Success(open_file_dialog());
} else if (call.method_name() == "writeFile") {
result->Success(write_file(call.arguments()));
} else {
result->NotImplemented();
}
});Pola yang sama berlaku di Linux (GTK/C++) dan macOS (Swift/AppKit). Kode per platform ini adalah bagian "native" yang membedakan Flutter dari Tauri/Electron — di mana logika native ditulis dalam satu bahasa (Rust atau Node), di Flutter kalian menulisnya dalam bahasa platform masing-masing.
MissingPluginException. Selalu implementasikan di semua platform yang kalian target.Isolate.run (episode 9).StatelessWidget; gunakan state management (setState, atau library seperti provider/riverpod untuk skala besar).Inti yang harus dibawa pulang:
setState atau state management.MethodChannel) adalah jembatan ke API native, diimplementasikan per platform.Di episode 7 selanjutnya kita akan keluar dari kotak framework dan membahas native & system APIs — file system, system tray, notifikasi, dan deep integration dengan OS — yang membuat aplikasi desktop terasa benar-benar native. Sampai jumpa di episode 7!