Skip to content

Commit 56148bb

Browse files
authored
Add native timezone support for Date and DateTime pickers (#122)
Introduce native support for `TZDateTime` in Date and DateTime pickers, enhancing the user experience by allowing timezone-aware date and time selection. Update dependencies and modify relevant components to integrate timezone functionality.
2 parents 6fb858d + aac49fe commit 56148bb

18 files changed

Lines changed: 286 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# Changelog
22

3+
## 7.5.16
4+
5+
- Native support for `TZDateTime` on Date and DateTime pickers.
6+
37
## 7.5.15
8+
49
- Adjusted `ThemedDateTimeSteppedPicker` to call the onChanged method only once after the time is selected.
510

611
## 7.5.14
12+
713
- Created `ThemedDateTimeSteppedPicker` widget for a stepped date and time picking experience, allowing users to select date first and then time
814

915
## 7.5.13

example/lib/main.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import 'package:layrz_theme_example/router.dart';
66
import 'package:layrz_theme_example/store/store.dart';
77
import 'package:shared_preferences/shared_preferences.dart';
88
import 'package:layrz_state/layrz_state.dart';
9+
import 'package:layrz_theme_example/timezone/native.dart'
10+
if (dart.library.js_interop) 'package:layrz_theme_example/timezone/web.dart';
911

1012
const font = AppFont(source: .google, name: 'Ubuntu Mono');
1113

1214
Future<void> main() async {
1315
WidgetsFlutterBinding.ensureInitialized();
14-
1516
await ThemedFontHandler.preloadFont(font);
17+
await initializeTimeZone();
1618

1719
final prefs = await SharedPreferences.getInstance();
1820
final rawThemeMode = prefs.getString('layrz.theme.mode');

example/lib/router.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ final goRoutes = [
145145
];
146146

147147
final router = GoRouter(
148-
initialLocation: kDebugMode ? '/map/layer' : '/',
148+
initialLocation: kDebugMode ? '/inputs/selectors/datetime' : '/',
149149
errorPageBuilder: (context, state) => customTransitionBuilder(context, state, const NotFoundView()),
150150
routes: goRoutes,
151151
);

example/lib/timezone/native.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:timezone/standalone.dart' as tz;
3+
import 'package:timezone/data/latest_all.dart' as tzl;
4+
import 'package:flutter_timezone/flutter_timezone.dart';
5+
6+
export 'package:timezone/standalone.dart' show Location;
7+
8+
/// [initializeTimeZone] is a stub that should be implemented in platform-specific code to initialize the timezone data.
9+
Future<void> initializeTimeZone() async {
10+
debugPrint('layrz_utils/timezone: Initializing timezone data');
11+
try {
12+
await tz.initializeTimeZone('https://cdn.layrz.com/resources/utils/timezones-2025.tzf');
13+
} catch (err) {
14+
debugPrint(
15+
'layrz_utils/timezone: Error initializing timezone data from Layrz CDN, falling back to built-in data',
16+
);
17+
tzl.initializeTimeZones();
18+
}
19+
return Future.value();
20+
}
21+
22+
/// [getTimezone] is a stub that should be implemented in platform-specific code to get the current timezone.
23+
Future<tz.Location> getTimezone() async {
24+
debugPrint('layrz_utils/timezone: Getting current timezone');
25+
final tzinfo = await FlutterTimezone.getLocalTimezone();
26+
return tz.getLocation(tzinfo.identifier);
27+
}
28+
29+
/// [setTimezone] is a stub that should be implemented in platform-specific code to set the timezone.
30+
Future<void> setTimezone(tz.Location timezone) {
31+
final pre = DateTime.now().toIso8601String();
32+
tz.setLocalLocation(timezone);
33+
final post = tz.TZDateTime.now(timezone).toIso8601String();
34+
debugPrint('layrz_utils/timezone: Timezone changed from $pre to $post');
35+
return Future.value();
36+
}
37+
38+
/// [castTimezone] converts a timezone string to a [Location] object. If the input is null or invalid,
39+
/// it defaults to the device's timezone.
40+
Future<tz.Location> castTimezone(String? timezone) async {
41+
if (timezone == null) return await getTimezone();
42+
try {
43+
return tz.getLocation(timezone);
44+
} catch (e) {
45+
debugPrint('layrz_utils/timezone: Invalid timezone "$timezone", defaulting to browser timezone');
46+
return await getTimezone();
47+
}
48+
}
49+
50+
/// [getTimezones] gets the list of available timezones
51+
List<tz.Location> getTimezones() => tz.timeZoneDatabase.locations.values.toList();

example/lib/timezone/web.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:timezone/browser.dart' as tz;
3+
import 'package:timezone/data/latest_all.dart' as tzl;
4+
import 'package:flutter_timezone/flutter_timezone.dart';
5+
6+
export 'package:timezone/browser.dart' show Location;
7+
8+
/// [initializeTimeZone] is a stub that should be implemented in platform-specific code to initialize the timezone data.
9+
Future<void> initializeTimeZone() async {
10+
debugPrint('layrz_utils/timezone: Initializing timezone data');
11+
try {
12+
await tz.initializeTimeZone('https://cdn.layrz.com/resources/utils/timezones-2025.tzf');
13+
} catch (err) {
14+
debugPrint(
15+
'layrz_utils/timezone: Error initializing timezone data from Layrz CDN, falling back to built-in data',
16+
);
17+
tzl.initializeTimeZones();
18+
}
19+
return Future.value();
20+
}
21+
22+
/// [getTimezone] is a stub that should be implemented in platform-specific code to get the current timezone.
23+
Future<tz.Location> getTimezone() async {
24+
debugPrint('layrz_utils/timezone: Getting current timezone');
25+
final tzinfo = await FlutterTimezone.getLocalTimezone();
26+
return tz.getLocation(tzinfo.identifier);
27+
}
28+
29+
/// [setTimezone] is a stub that should be implemented in platform-specific code to set the timezone.
30+
Future<void> setTimezone(tz.Location timezone) {
31+
final pre = DateTime.now().toIso8601String();
32+
tz.setLocalLocation(timezone);
33+
final post = tz.TZDateTime.now(timezone).toIso8601String();
34+
debugPrint('layrz_utils/timezone: Timezone changed from $pre to $post');
35+
return Future.value();
36+
}
37+
38+
/// [castTimezone] converts a timezone string to a [Location] object. If the input is null or invalid,
39+
/// it defaults to the device's timezone.
40+
Future<tz.Location> castTimezone(String? timezone) async {
41+
if (timezone == null) return await getTimezone();
42+
try {
43+
return tz.getLocation(timezone);
44+
} catch (e) {
45+
debugPrint('layrz_utils/timezone: Invalid timezone "$timezone", defaulting to browser timezone');
46+
return await getTimezone();
47+
}
48+
}
49+
50+
/// [getTimezones] gets the list of available timezones
51+
List<tz.Location> getTimezones() => tz.timeZoneDatabase.locations.values.toList();

example/lib/views/inputs/inputs.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
library;
22

3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/material.dart';
45
import 'package:flutter/services.dart';
56
import 'package:layrz_icons/layrz_icons.dart';
67
import 'package:layrz_theme/layrz_theme.dart';
78
import 'package:layrz_theme_example/store/store.dart';
9+
import 'package:timezone/standalone.dart';
10+
import 'package:timezone/timezone.dart';
11+
12+
import 'package:layrz_theme_example/timezone/native.dart'
13+
if (dart.library.js_interop) 'package:layrz_theme_example/timezone/web.dart';
814

915
part 'src/buttons.dart';
1016
part 'src/calendar.dart';

example/lib/views/inputs/src/buttons.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ class _ButtonsViewState extends State<ButtonsView> {
466466
}
467467

468468
Widget _factorButton({
469-
required style,
469+
required ThemedButtonStyle style,
470470
ThemedTooltipPosition tooltipPosition = ThemedTooltipPosition.right,
471471
}) {
472472
return ThemedButton(

example/lib/views/inputs/src/selectors/datetime.dart

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ class _DateTimePickersViewState extends State<DateTimePickersView> {
2020
ThemedMonth? _selectedMonth;
2121
List<ThemedMonth> _selectedMonthRange = [];
2222

23+
late Location tz;
24+
@override
25+
void initState() {
26+
super.initState();
27+
tz = getLocation('Asia/Tokyo');
28+
setTimezone(tz);
29+
}
30+
2331
@override
2432
Widget build(BuildContext context) {
2533
return Layout(
@@ -43,10 +51,20 @@ class _DateTimePickersViewState extends State<DateTimePickersView> {
4351
),
4452
const SizedBox(height: 10),
4553
const Text("Classic picker"),
54+
4655
ThemedDatePicker(
4756
labelText: "Example label",
4857
value: _selectedDate,
49-
onChanged: (val) => setState(() => _selectedDate = val),
58+
onChanged: (val) {
59+
setState(() => _selectedDate = val);
60+
if (val is TZDateTime) {
61+
debugPrint('Selected date: $val (TZDateTime) in timezone ${tz.name}');
62+
_selectedDate = val;
63+
} else {
64+
_selectedDate = TZDateTime.from(val, tz);
65+
debugPrint('Selected date: $_selectedDate (converted to TZDateTime) in timezone ${tz.name}');
66+
}
67+
},
5068
),
5169
const Text("And the range variant"),
5270
ThemedDateRangePicker(
@@ -99,16 +117,54 @@ class _DateTimePickersViewState extends State<DateTimePickersView> {
99117
),
100118
const SizedBox(height: 10),
101119
const Text("Classic picker"),
120+
if (kDebugMode) ...[
121+
if (_selectedDateTime is TZDateTime)
122+
Text('Selected date and time: $_selectedDateTime (TZDateTime) in timezone ${tz.name}')
123+
else if (_selectedDateTime != null)
124+
Text('Selected date and time: $_selectedDateTime (DateTime, not converted to TZDateTime)'),
125+
],
102126
ThemedDateTimePicker(
103127
labelText: "Example label",
104128
value: _selectedDateTime,
105-
onChanged: (val) => setState(() => _selectedDateTime = val),
129+
onChanged: (val) {
130+
debugPrint("Raw selected date and time: $val (${val.runtimeType})");
131+
if (val is TZDateTime) {
132+
debugPrint('Selected date and time: $val (TZDateTime) in timezone ${tz.name}');
133+
setState(() => _selectedDateTime = val);
134+
} else {
135+
final converted = TZDateTime.from(val, tz);
136+
debugPrint('Selected date and time: $converted (converted to TZDateTime) in timezone ${tz.name}');
137+
setState(() => _selectedDateTime = converted);
138+
}
139+
},
106140
),
107141
const Text("And the range variant"),
142+
if (kDebugMode) ...[
143+
for (final entry in _selectedDateTimeRange.asMap().entries) ...[
144+
if (entry.value is TZDateTime) ...[
145+
Text('[${entry.key}] Selected date and time: ${entry.value} (TZDateTime) in timezone ${tz.name}'),
146+
] else ...[
147+
Text('[${entry.key}] Selected date and time: ${entry.value} (DateTime, not converted to TZDateTime)'),
148+
],
149+
],
150+
],
108151
ThemedDateTimeRangePicker(
109152
labelText: "Example label",
110153
value: _selectedDateTimeRange,
111-
onChanged: (val) => setState(() => _selectedDateTimeRange = val),
154+
onChanged: (val) {
155+
debugPrint("Raw selected date and time range: $val");
156+
final convertedRange = val.map((dateTime) {
157+
if (dateTime is TZDateTime) {
158+
debugPrint('Selected date and time: $dateTime (TZDateTime) in timezone ${tz.name}');
159+
return dateTime;
160+
} else {
161+
final converted = TZDateTime.from(dateTime, tz);
162+
debugPrint('Selected date and time: $converted (converted to TZDateTime) in timezone ${tz.name}');
163+
return converted;
164+
}
165+
}).toList();
166+
setState(() => _selectedDateTimeRange = convertedRange);
167+
},
112168
),
113169
const Text("Stepped variant, after selecting the date, you will select the time"),
114170
ThemedDateTimeSteppedPicker(

example/lib/views/map/src/layer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class _MapLayerViewState extends State<MapLayerView> with TickerProviderStateMix
3535
super.dispose();
3636
}
3737

38-
void _listener(event) {
38+
void _listener(ThemedMapEvent event) {
3939
debugPrint('layrz_theme_example/ThemedMapController() event: $event');
4040
}
4141

example/pubspec.lock

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ packages:
121121
url: "https://pub.dev"
122122
source: hosted
123123
version: "0.9.9"
124+
equatable:
125+
dependency: transitive
126+
description:
127+
name: equatable
128+
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
129+
url: "https://pub.dev"
130+
source: hosted
131+
version: "2.0.8"
124132
fake_async:
125133
dependency: transitive
126134
description:
@@ -202,10 +210,10 @@ packages:
202210
dependency: "direct dev"
203211
description:
204212
name: flutter_lints
205-
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
213+
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
206214
url: "https://pub.dev"
207215
source: hosted
208-
version: "5.0.0"
216+
version: "6.0.0"
209217
flutter_map:
210218
dependency: "direct main"
211219
description:
@@ -243,6 +251,14 @@ packages:
243251
description: flutter
244252
source: sdk
245253
version: "0.0.0"
254+
flutter_timezone:
255+
dependency: "direct main"
256+
description:
257+
name: flutter_timezone
258+
sha256: "978192f2f9ea6d019a4de4f0211d76a9af955ca24865828fa98ca4e20cf0cb3c"
259+
url: "https://pub.dev"
260+
source: hosted
261+
version: "5.0.1"
246262
flutter_web_plugins:
247263
dependency: transitive
248264
description: flutter
@@ -374,7 +390,7 @@ packages:
374390
path: ".."
375391
relative: true
376392
source: path
377-
version: "7.5.14"
393+
version: "7.5.16"
378394
leak_tracker:
379395
dependency: transitive
380396
description:
@@ -411,10 +427,10 @@ packages:
411427
dependency: transitive
412428
description:
413429
name: lints
414-
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
430+
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
415431
url: "https://pub.dev"
416432
source: hosted
417-
version: "5.1.1"
433+
version: "6.1.0"
418434
lists:
419435
dependency: transitive
420436
description:
@@ -804,6 +820,14 @@ packages:
804820
url: "https://pub.dev"
805821
source: hosted
806822
version: "0.7.9"
823+
timezone:
824+
dependency: "direct main"
825+
description:
826+
name: timezone
827+
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
828+
url: "https://pub.dev"
829+
source: hosted
830+
version: "0.10.1"
807831
two_dimensional_scrollables:
808832
dependency: transitive
809833
description:

0 commit comments

Comments
 (0)