Themely: Cara Paling Clean untuk Implementasi Dark Mode di Flutter
Implementasi dark mode di Flutter ternyata tidak sesimpel kelihatannya. Dari manage ThemeData, nyimpen preferensi user, bikin animasi transisi, sampai conditional rendering yang berserakan di mana-mana, semuanya bisa jadi berantakan kalau tidak punya sistem yang tepat. Artikel ini membahas bagaimana package Themely bisa menyelesaikan semua itu dengan setup minimal, API yang intuitif, dan tanpa perlu refactor struktur yang sudah ada.
Kamu punya aplikasi Flutter yang sudah jalan. Fitur utama sudah selesai, UI sudah rapi. Lalu muncul satu permintaan yang terdengar simpel: "bisa tambah dark mode?"
Kamu pikir ini mudah. Ternyata tidak.
Mulai dari manage ThemeData di mana-mana, bikin Provider sendiri, nulis ternary operator untuk setiap warna, sampai pusing karena preferensi user hilang setiap kali app direstart. Belum lagi kalau client minta ada animasi transisi saat ganti tema, atau minta ada jadwal otomatis dark mode mulai jam 6 sore.
Setelah browsing beberapa solusi, saya ketemu package bernama Themely buatan Dimas Febriyanto. Ini catatan saya selama implementasi.

Kenapa Tidak Pakai Cara Biasa?
Cara konvensional biasanya begini: simpan state tema di Provider atau Riverpod, pass ThemeData ke MaterialApp, lalu di setiap widget yang butuh beda tampilan antara dark dan light, tulis kondisi manual.
Masalahnya mulai terasa begitu skala app bertambah. Ternary operator muncul di mana-mana. Tidak ada konsistensi token warna. Preferensi tidak tersimpan. Dan kalau mau tambah animasi transisi, harus bungkus sendiri dengan AnimatedSwitcher atau sejenisnya.
Themely menyelesaikan semua itu dalam satu package.
Instalasi
Syarat minimal sebelum mulai:
Flutter >= 3.10.0
Dart >= 3.0.0
Tambahkan ke pubspec.yaml:
dependencies:
themely: ^1.0.0
Langkah Pertama: Setup di main.dart
Hal pertama yang perlu dilakukan adalah inisialisasi ThemeController. Ini adalah pusat kendali semua urusan tema di Themely. Karena dia perlu load preferensi yang tersimpan sebelumnya, inisialisasinya harus di-await sebelum runApp dipanggil.
import 'package:flutter/material.dart';
import 'package:themely/themely.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final controller = ThemeController(
lightTheme: ThemeData.light(),
darkTheme: ThemeData.dark(),
);
await controller.initialize();
runApp(MyApp(controller: controller));
}
Tidak ada magic di sini. Controller menerima lightTheme dan darkTheme biasa. Kamu bisa passing ThemeData custom yang sudah kamu buat sebelumnya tanpa perlu refactor apapun.
Bungkus MaterialApp dengan ThemelyApp
Langkah berikutnya adalah membungkus MaterialApp dengan ThemelyApp. Widget ini yang akan menginjeksi tema ke seluruh widget tree dan menangani stream perubahan mode secara otomatis.
class MyApp extends StatelessWidget {
final ThemeController controller;
const MyApp({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return ThemelyApp(
controller: controller,
builder: (context, theme, child) => MaterialApp(
theme: theme,
home: const Home(),
),
);
}
}
Selesai untuk setup awal. Sekarang seluruh app sudah terhubung ke sistem tema Themely.
Tombol Toggle Dark Mode
Bagian yang paling sering ditanya: gimana cara toggle-nya? Cukup satu baris lewat context extension.
ElevatedButton(
onPressed: () => context.themeController.toggleDark(),
child: Text(context.isDark ? 'Switch to Light' : 'Switch to Dark'),
)
Tidak ada setState. Tidak ada notifyListeners. Themely menangani rebuild secara otomatis lewat stream internal.
Masalah Klasik: Warna Hardcode di Mana-mana
Ini masalah yang paling sering saya temui di proyek lama. Ada ratusan Colors.white dan Colors.black yang tersebar di seluruh codebase. Begitu dark mode diaktifkan, banyak bagian UI yang tidak ikut berubah karena warnanya hardcode.
Themely punya solusi namanya semantic color tokens. Daripada hardcode warna, kamu akses warna lewat token semantik yang otomatis menyesuaikan diri dengan mode aktif.
Container(
color: context.themeColors.cardSurface,
child: Text(
'Konten kartu',
style: TextStyle(
color: context.themeColors.buttonBackground,
),
),
)
Begitu user ganti ke dark mode, semua widget yang pakai context.themeColors langsung rebuild dengan nilai warna yang tepat. Tidak perlu sentuh satu widget pun secara manual.
Animasi Transisi Gratis
Salah satu hal yang biasanya butuh effort ekstra adalah animasi saat ganti tema. Dengan Themely, animasi ini sudah built-in via AnimatedTheme. Kamu bahkan bisa kustomisasi durasi dan kurva animasinya.
final controller = ThemeController(
lightTheme: ThemeData.light(),
darkTheme: ThemeData.dark(),
animationDuration: const Duration(milliseconds: 400),
animationCurve: Curves.easeInOutBack,
);Preferensi Tersimpan Otomatis
Ini fitur yang sering lupa diimplementasi manual: menyimpan pilihan tema user supaya tidak reset setiap kali app dibuka ulang. Themely menangani ini otomatis via SharedPreferences. Tidak perlu setup tambahan apapun. Cukup panggil controller.initialize() seperti di langkah pertama tadi, dan preferensi akan di-load otomatis.
Kalau kamu tidak mau pakai SharedPreferences dan punya storage sendiri seperti Hive atau Isar, bisa buat adapter custom:
class HiveThemeStorage implements ThemeStorage {
@override
Future<String?> loadMode(String key) async {
return Hive.box('settings').get(key);
}
@override
Future<void> saveMode(String key, String mode) async {
await Hive.box('settings').put(key, mode);
}
}
final controller = ThemeController(storage: HiveThemeStorage());Widget Switcher: Tidak Perlu Ternary Lagi
Masalah lain yang sering muncul adalah conditional rendering untuk aset atau ikon yang berbeda antara dark dan light mode. Biasanya ditulis begini:
Icon(context.isDark ? Icons.nightlight_round : Icons.wb_sunny)Kalau ini muncul di puluhan tempat, codebase jadi susah dibaca. Themely punya widget bawaan untuk ini:
ThemeIcon(
light: Icons.wb_sunny,
dark: Icons.nightlight_round,
)
ThemeAsset(
light: AssetImage('assets/logo_light.png'),
dark: AssetImage('assets/logo_dark.png'),
)
ThemeText(
light: 'Selamat Pagi',
dark: 'Selamat Malam',
)Untuk yang pakai animasi Lottie, ada ThemeLottie:
ThemeLottie.asset(
light: 'assets/animations/sun.json',
dark: 'assets/animations/moon.json',
)Bonus: Auto Schedule Dark Mode
Client minta dark mode aktif otomatis mulai jam 6 sore? Tidak perlu buat timer sendiri. Themely sudah punya fitur ini:
final controller = ThemeController(
lightTheme: ThemeData.light(),
darkTheme: ThemeData.dark(),
autoSchedule: true,
darkFrom: const TimeOfDay(hour: 18, minute: 0),
darkUntil: const TimeOfDay(hour: 6, minute: 0),
);Kalau mau logika yang lebih custom, misalnya ganti ke AMOLED mode saat baterai hemat aktif, bisa bypass scheduler bawaan:
Battery().onBatteryStateChanged.listen((state) {
if (state == BatteryState.powerSave) {
context.themeController.setMode(AppThemeMode.amoled);
}
});Preview Mode Sebelum Disimpan
Fitur ini berguna kalau kamu mau beri user opsi untuk mencoba tema dulu sebelum benar-benar menyimpannya, mirip seperti preview wallpaper di sistem operasi.
// Tampilkan preview tanpa menyimpan
context.themeController.preview(AppThemeMode.dark);
// Kalau user setuju, simpan
context.themeController.confirmPreview();
// Kalau tidak, kembalikan ke semula
context.themeController.cancelPreview();Teks yang Selalu Terbaca
Masalah klasik dark mode lainnya: teks yang jadi tidak terbaca karena warna tidak kontras dengan background. Themely punya helper contrastOn yang otomatis memilih antara hitam atau putih berdasarkan luminance background sesuai standar WCAG.
Text(
'Teks ini selalu terbaca',
style: TextStyle(
color: context.themeColors.contrastOn(context.themeColors.buttonBackground),
),
)Kesimpulan
Implementasi dark mode yang terlihat simpel ternyata punya banyak detail yang perlu diperhatikan: persistence, token warna yang konsisten, animasi transisi, conditional rendering, sampai jadwal otomatis. Menangani semua itu dari nol butuh waktu dan effort yang tidak sedikit.
Themely mengemas semua kebutuhan itu dalam satu package yang rapi. Setup-nya cepat, API-nya intuitif, dan tidak memaksa kamu refactor struktur yang sudah ada.
Kalau kamu sedang dalam posisi yang sama seperti saya tadi, coba cek langsung repositorinya di github.com/dimassfeb-09/themely.

Dimas Febriyanto
Fullstack & Mobile Developer specializing in Golang & Flutter. Aktif sebagai Junior Mobile Developer di Sagara Technology & asisten lab di Universitas Gunadarma. Passionate about building scalable algorithms & high-impact applications.
Lihat Portofolio