Introduction
In modern mobile development, a responsive user interface goes beyond layout adaptations—it includes dynamic theme changes aligned with user preferences or time-based triggers. Implementing dark mode scheduling in Flutter enhances user comfort during evening hours and conserves battery life on OLED devices. This tutorial walks you through configuring a time-based theme switcher that automatically toggles between light and dark modes.
Understanding Dark Mode Scheduling
Before writing code, define your scheduling criteria. Common approaches include:
Time-based switching: Changes theme at specific hours (e.g., 7 PM to 7 AM).
Sunset/Sunrise API: Calculates local dusk and dawn times for accuracy.
For simplicity, this guide focuses on static time-based scheduling. Conceptually, you’ll:
Capture the system clock.
Evaluate if the current time falls within your “dark” interval.
Trigger a theme update.
A central ThemeScheduler class can encapsulate these responsibilities and broadcast updates via Flutter’s ChangeNotifier.
Building the Theme Scheduler
Create a Dart file theme_scheduler.dart. Implement a ChangeNotifier that checks the clock at app startup and whenever the system resumes.
import 'dart:async';
import 'package:flutter/material.dart';
class ThemeScheduler extends ChangeNotifier {
bool _isDarkMode;
Timer? _timer;
ThemeScheduler() : _isDarkMode = false {
_evaluateTheme();
}
bool get isDarkMode => _isDarkMode;
void _evaluateTheme() {
final now = TimeOfDay.now();
final darkStart = TimeOfDay(hour: 19, minute: 0);
final darkEnd = TimeOfDay(hour: 7, minute: 0);
_isDarkMode = now.hour >= darkStart.hour || now.hour < darkEnd.hour;
notifyListeners();
_timer?.cancel();
_timer = Timer(Duration(minutes: 30), _evaluateTheme);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
} This scheduler:
Evaluates the theme immediately.
Sets a periodic timer to re-evaluate every 30 minutes.
Notifies listeners on changes.
Applying Themes in the App
Wrap your MaterialApp with a ChangeNotifierProvider (from the provider package) to supply ThemeScheduler throughout the widget tree. Use a Consumer or Selector to rebuild the MaterialApp when isDarkMode toggles.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_scheduler.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeScheduler(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = context.watch<ThemeScheduler>().isDarkMode;
return MaterialApp(
title: 'Dark Mode Scheduler',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: isDark ? ThemeMode.dark : ThemeMode.light,
home: const HomeScreen(),
);
}
} Key points:
themeMode dynamically switches based on scheduler state.
Widgets rebuild only when isDarkMode changes.
Testing and Edge Cases
Automated and manual tests ensure reliability:
Unit Tests: Mock system time to simulate intervals before and after your dark-mode window. Verify _evaluateTheme sets _isDarkMode appropriately.
Resuming from Background: Android and iOS might pause timers when the app is suspended. Use WidgetsBindingObserver to call _evaluateTheme on resumed events.
class AppLifecycleHandler extends StatefulWidget {
final Widget child;
const AppLifecycleHandler({required this.child});
@override
_AppLifecycleHandlerState createState() => _AppLifecycleHandlerState();
}
class _AppLifecycleHandlerState extends State<AppLifecycleHandler>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
context.read<ThemeScheduler>()._evaluateTheme();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}Wrap MyApp in AppLifecycleHandler to refresh the scheduler on resume. Also consider device time zone changes, which may require listening for PlatformDispatcher.instance.onMetricsChanged or similar.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
By leveraging a ChangeNotifier, timed evaluation, and Flutter’s lifecycle observers, you can implement seamless dark mode scheduling. This approach requires minimal boilerplate, relies on Flutter’s core libraries, and improves user experience by adapting the interface to the time of day.