flutter_timezone_observer
A Flutter package for accessing the native device timezone (IANA) and observing changes. Includes mixins to automatically handle app lifecycle events.
Features
- Get Current Timezone: One-time async call to get the current IANA timezone (e.g.,
America/New_York). - Listen to Changes: Get a
Streamof timezone changes reported by the platform. - Lifecycle-Aware Mixin: Includes
AppLifecycleAwareTimezoneObserverMixinto automatically re-fetch the timezone when the app resumes. - Simple Mixin: Includes a basic
TimezoneObserverMixinfor simple use cases. - Minimal: No external Flutter package dependencies.
Platform Support
| Platform | Timezone source | Change detection |
|---|---|---|
| Android | ZoneId.systemDefault() |
ACTION_TIMEZONE_CHANGED broadcast |
| iOS | TimeZone.current.identifier |
NSSystemTimeZoneDidChange notification |
| Web | Intl.DateTimeFormat().resolvedOptions().timeZone |
Re-read on visibilitychange / focus, plus polling every 5s while visible |
The public API is identical on every platform — see the notes on Web for the differences in how changes are detected there.
How to Use
1. One-time Read
Get the current timezone on app start or any time you need it.
import 'package.flutter_timezone_observer/flutter_timezone_observer.dart';
Future<void> main() async {
final String timezone = await FlutterTimezoneObserver.currentTimezone;
print('Current device timezone: $timezone');
runApp(MyApp());
}
2. Listen to Stream
Listen to the native broadcast for timezone changes.
final subscription = FlutterTimezoneObserver.onTimezoneChanged.listen((zone) {
print('Timezone changed: $zone');
});
// Don't forget to cancel
subscription.cancel();
3. Usage with Lifecycle-Aware Mixin (Recommended)
This is the most reliable way. Use this mixin on your State to automatically handle initialization, stream listening, and app-resume logic.
import 'package.flutter/material.dart';
import 'package:flutter_timezone_observer/app_lifecycle_aware_timezone_observer_mixin.dart';
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget>
with AppLifecycleAwareTimezoneObserverMixin {
@override
void onTimezoneChanged(String newZone) {
// The mixin handles all the logic.
// Just call setState to rebuild with the new value.
setState(() {
// `currentZone` (from the mixin) is already updated
});
}
@override
Widget build(BuildContext context) {
// `currentZone` is provided by the mixin
return Text(
'Current Zone: ${currentZone ?? 'Loading...'}',
);
}
}
4. Usage with Simple Mixin (Basic)
If you only care about the stream and don't need the app-resume logic, use TimezoneObserverMixin.
import 'package:flutter_timezone_observer/timezone_observer_mixin.dart';
class MyWidget extends StatefulWidget {
// ...
}
class _MyWidgetState extends State<MyWidget>
with TimezoneObserverMixin { // <-- Note: the simple mixin
@override
void onTimezoneChanged(String newZone) {
setState(() {});
}
// ...
}
5. Advanced: Handling Paused State
By default, AppLifecycleAwareTimezoneObserverMixin ignores stream events while the app is paused (to prevent background setState calls), as it re-fetches the timezone on resume anyway.
You can override this behavior by setting emitTimezoneWhenPaused to true.
import 'package.flutter/material.dart';
import 'package:flutter_timezone_observer/app_lifecycle_aware_timezone_observer_mixin.dart';
class MyWidget extends StatefulWidget {
// ...
}
class _MyWidgetState extends State<MyWidget>
with AppLifecycleAwareTimezoneObserverMixin {
// Override the getter to enable background updates
@override
bool get emitTimezoneWhenPaused => true;
@override
void onTimezoneChanged(String newZone) {
// This will now fire even if the app is paused.
}
@override
Widget build(BuildContext context) {
return Text(
'Current Zone: ${currentZone ?? 'Loading...'}',
);
}
}
Web
Browsers do not broadcast a "timezone changed" event, so the change cannot be
observed the way it is on Android and iOS. Instead, the web implementation
re-reads Intl.DateTimeFormat().resolvedOptions().timeZone:
- Immediately when the page becomes visible again or the window regains focus — the usual path when the user changes the timezone in the OS settings and switches back to the browser.
- Every 5 seconds while the page is visible. Polling is suspended while the page is hidden, since browsers throttle timers in background tabs anyway.
onTimezoneChanged only emits when the value actually differs from the
previously seen one, so the polling is invisible to your listeners.
Nothing has to be registered manually: the plugin is wired up by Flutter's web
plugin registrant. Both dart2js and dart2wasm builds are supported.
Notes and limitations
- Detection depends on the browser refreshing the timezone it has resolved for the page. Current versions of Chrome, Firefox and Safari do that when the OS timezone changes; a browser that only refreshes on reload cannot be observed by any polling strategy.
- The mixins work on web, but through the stream rather than through the app
lifecycle: Flutter's web engine never reports
AppLifecycleState.paused(it reportshiddeninstead), so inAppLifecycleAwareTimezoneObserverMixinthe re-fetch on resume never runs andemitTimezoneWhenPausedhas no effect there. Thevisibilitychangehandling above covers the same scenario, so returning to the tab still delivers the new timezone.
If the default cadence does not fit your app, you can install the web implementation yourself with a different one — for example, to rely on visibility and focus events only:
// web_timezone_setup.dart — import this only from web-specific code.
import 'package:flutter_timezone_observer/flutter_timezone_observer_platform_interface.dart';
import 'package:flutter_timezone_observer/flutter_timezone_observer_web.dart';
void setUpTimezoneObserver() {
FlutterTimezoneObserverPlatform.instance = FlutterTimezoneObserverWeb(
pollInterval: Duration.zero, // or any custom interval
);
}
Installation
1. Add to pubspec.yaml
dart pub add flutter_timezone_observer