Topik ini membahas penyediaan tema gelap pada aplikasi Flutter, mulai dari properti darkTheme dan themeMode pada MaterialApp, skema warna terpisah untuk mode terang dan gelap, pembacaan kecerahan sistem, hingga penggantian themeMode secara dinamis dari dalam aplikasi. Aplikasi yang mengikuti pola di sini tampil nyaman pada mode gelap tanpa menulis ulang warna di setiap widget, karena seluruh tampilan tetap membaca peran warna dari tema.
Properti darkTheme dan themeMode#
MaterialApp menyediakan dua slot tema, yaitu theme untuk mode terang dan darkTheme untuk mode gelap. Properti themeMode menentukan slot mana yang digunakan dan menerima tiga nilai dari enum ThemeMode, yaitu ThemeMode.system, ThemeMode.light, dan ThemeMode.dark. Nilai bawaannya adalah ThemeMode.system, sehingga aplikasi tanpa pengaturan apa pun sudah mengikuti kecerahan yang dipilih pengguna pada perangkat.
// tidak-dijalankan
MaterialApp(
title: 'Catatan Harian',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: LayarCatatan(),
)Kedua slot menggunakan seedColor yang sama, sehingga identitas warna aplikasi tetap terasa satu keluarga pada kedua mode. ColorScheme.fromSeed menghasilkan skema gelap apabila argumen brightness diberi nilai Brightness.dark, termasuk menghitung ulang pasangan warna teks dan latarnya untuk latar gelap. Widget di bawah MaterialApp tidak perlu mengetahui mode mana yang sedang aktif, karena peran warna yang dibaca tetap bernama sama pada kedua mode.
Skema warna terang dan gelap yang terpisah#
Mode gelap bukan sekadar pertukaran warna terang dengan gelap pada satu skema. Kontras yang nyaman pada latar putih belum tentu nyaman pada latar gelap, dan Material 3 menghitung pasangan warna teks dan latar secara terpisah untuk masing-masing kecerahan. Karena itu kedua slot menerima ThemeData yang berdiri sendiri, meskipun keduanya berasal dari seedColor yang sama.
Apabila satu peran perlu dibedakan antara mode terang dan gelap, misalnya warna aksen kartu terpilih, skema pada slot tersebut dapat disalin dengan copyWith, lalu peran yang bersangkutan ditimpa.
// tidak-dijalankan
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
).copyWith(
secondaryContainer: Colors.teal.shade900,
),
),Penimpaan pada contoh itu hanya menyentuh skema gelap, sedangkan skema terang mempertahankan nilai dari ColorScheme.fromSeed. Pola ini memanfaatkan copyWith yang sama seperti pada topik Theming, sehingga satu peran berubah tanpa menyusun ulang seluruh skema. Perbedaan peran yang kecil antara dua mode lazim dilakukan, sedangkan penggunaan dua kelompok warna yang sama sekali berbeda sebaiknya dihindari karena merusak kesatuan identitas aplikasi. Seed color yang sama pada kedua slot juga memastikan pergantian mode tidak terasa seperti berpindah aplikasi.
Membaca kecerahan sistem#
MediaQuery.platformBrightnessOf(context) membaca kecerahan (platform brightness) yang dipilih pengguna pada perangkat. Nilainya berupa Brightness.light atau Brightness.dark, dan framework memperbaruinya secara otomatis ketika pengguna mengubah pengaturan sistem, karena pembacaan dilakukan melalui MediaQuery yang memicu rebuild pada widget yang menggunakannya. Nilai ini berguna ketika tampilan perlu menyesuaikan diri dengan lingkungan sistem di luar warna tema, misalnya memilih ikon atau teks bantuan.
// tidak-dijalankan
final gelap = MediaQuery.platformBrightnessOf(context) == Brightness.dark;
return ListTile(
leading: Icon(gelap ? Icons.dark_mode : Icons.light_mode),
title: const Text('Catatan kuliah'),
);Nilai platformBrightnessOf perlu dibedakan dari tema yang benar-benar aktif. Nilai yang dibaca metode tersebut menjawab pertanyaan keadaan sistem, sedangkan themeMode menjawab pertanyaan slot tema yang digunakan aplikasi. Apabila themeMode dipaksa ke ThemeMode.light, platformBrightnessOf tetap melaporkan keadaan gelap. Oleh karena itu, penentuan warna dalam metode build sebaiknya tetap membaca Theme.of(context), yang nilainya sudah disesuaikan dengan mode yang aktif.
Pembacaan semacam ini tepat untuk teks bantuan dan elemen pelengkap yang meniru keadaan sistem, sedangkan warna utama tampilan tetap diambil dari Theme.of(context). Dengan pembagian itu, pembacaan keadaan sistem dan pembacaan tema aktif tidak saling menggantikan. Nilai yang dibaca juga selalu terbarui pada setiap build, sehingga perubahan pengaturan sistem langsung terlihat tanpa langkah tambahan.
Mengganti themeMode secara dinamis#
Pengalih tema paling sederhana menyimpan ThemeMode sebagai state pada widget yang membangun MaterialApp, karena properti themeMode dibaca pada widget tersebut. Perubahan state melalui setState memicu rebuild MaterialApp, dan seluruh layar berganti tema seketika tanpa menyentuh widget di bawahnya. Pola menaikkan state ke widget induk yang diperkenalkan pada modul Interaktivitas dan state digunakan kembali di sini, dengan callback sebagai jembatan dari layar pengatur menuju state aplikasi.
// tidak-dijalankan
import 'package:flutter/material.dart';
void main() => runApp(const AplikasiCatatan());
class AplikasiCatatan extends StatefulWidget {
const AplikasiCatatan({super.key});
@override
State<AplikasiCatatan> createState() => _AplikasiCatatanState();
}
class _AplikasiCatatanState extends State<AplikasiCatatan> {
ThemeMode _mode = ThemeMode.system;
void _gantiMode(ThemeMode mode) {
setState(() {
_mode = mode;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Catatan Harian',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
),
themeMode: _mode,
home: LayarCatatan(mode: _mode, gantiMode: _gantiMode),
);
}
}
class LayarCatatan extends StatelessWidget {
const LayarCatatan({super.key, required this.mode, required this.gantiMode});
final ThemeMode mode;
final ValueChanged<ThemeMode> gantiMode;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Catatan Harian'),
),
body: Center(
child: SegmentedButton<ThemeMode>(
segments: const [
ButtonSegment(
value: ThemeMode.system,
icon: Icon(Icons.brightness_auto),
label: Text('Sistem'),
),
ButtonSegment(
value: ThemeMode.light,
icon: Icon(Icons.light_mode),
label: Text('Terang'),
),
ButtonSegment(
value: ThemeMode.dark,
icon: Icon(Icons.dark_mode),
label: Text('Gelap'),
),
],
selected: {mode},
onSelectionChanged: (pilihan) => gantiMode(pilihan.first),
),
),
);
}
}Kelas AplikasiCatatan berperan sebagai StatefulWidget akar yang menyimpan _mode, sedangkan LayarCatatan menerima mode tersebut bersama callback gantiMode. SegmentedButton menampilkan tiga pilihan mode dan menandai pilihan yang sedang aktif melalui selected, sehingga pengguna selalu melihat keadaan pengaturan saat itu. Ketukan pada salah satu segmen memanggil gantiMode, lalu setState di dalamnya memperbarui _mode dan menjadwalkan rebuild MaterialApp. Warna yang berganti bukan hasil penghitungan ulang di setiap widget, melainkan hasil pergantian ThemeData yang terjadi di satu tempat. Untuk aplikasi yang pengaturannya digunakan di banyak tempat, state ThemeMode ini dapat dinaikkan lagi ke state manager seperti Provider yang dibahas pada modul Provider dan state modern.
Kesalahan umum#
Kesalahan pada mode gelap umumnya berasal dari warna yang tidak dibaca dari tema, bukan dari slot tema yang salah. Tiga pola berikut paling sering muncul saat mode gelap pertama kali disiapkan.
- Menyediakan
darkThemetetapi menulis warna tetap di widget. Teks dengan Colors.black, misalnya, hilang di atas latar gelap karena warnanya tidak dibaca dari tema. Perbaikannya, ambil setiap warna dari peran ColorScheme melalui Theme.of(context). - Memaksa
themeModekeThemeMode.darktanpa mengisidarkTheme. MaterialApp menggunakanthemesebagai cadangan sehingga layar tidak pernah menjadi gelap. Perbaikannya, isi kedua slot sebelum membuka pengalih mode kepada pengguna. - Menentukan warna berdasarkan platformBrightnessOf. Nilai itu melaporkan keadaan sistem, bukan slot tema yang aktif, sehingga hasilnya keliru ketika pengguna memaksa mode tertentu. Perbaikannya, baca peran warna dari Theme.of(context) untuk kebutuhan tampilan.
Ringkasan#
darkThememenyimpan tema gelap, sedangkanthemeModememilih antaraThemeMode.system,ThemeMode.light, danThemeMode.dark.- Nilai bawaan
ThemeMode.systemmembuat aplikasi mengikuti kecerahan perangkat tanpa kode tambahan. - ColorScheme.fromSeed dengan argumen
Brightness.darkmenghasilkan skema gelap dari seed color yang sama. - MediaQuery.platformBrightnessOf(context) membaca kecerahan sistem, sedangkan Theme.of(context) membaca tema yang benar-benar aktif.
- Pengalih tema disimpan sebagai state pada widget yang membangun MaterialApp, diubah melalui setState atau state manager aplikasi.
Referensi#
- ThemeMode enum — Flutter API — Dokumentasi tiga nilai pemilih tema beserta perilaku bawaannya.
- ColorScheme class — Flutter API — Daftar peran warna yang dihitung ulang untuk kecerahan gelap.
- Use themes to share colors and font styles — Flutter — Resep resmi yang memuat penyiapan tema terang dan gelap pada satu aplikasi.
- platformBrightnessOf — Flutter API — Dokumentasi metode pembaca kecerahan sistem beserta perilaku pembaruan nilainya.